1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "editor.h"
5
6#include "auto_map.h"
7#include "editor_actions.h"
8
9#include <base/color.h>
10#include <base/dbg.h>
11#include <base/fs.h>
12#include <base/io.h>
13#include <base/log.h>
14#include <base/mem.h>
15#include <base/str.h>
16#include <base/time.h>
17
18#include <engine/client.h>
19#include <engine/engine.h>
20#include <engine/font_icons.h>
21#include <engine/gfx/image_loader.h>
22#include <engine/gfx/image_manipulation.h>
23#include <engine/graphics.h>
24#include <engine/input.h>
25#include <engine/keys.h>
26#include <engine/shared/config.h>
27#include <engine/storage.h>
28#include <engine/textrender.h>
29
30#include <generated/client_data.h>
31
32#include <game/client/components/camera.h>
33#include <game/client/gameclient.h>
34#include <game/client/lineinput.h>
35#include <game/client/ui.h>
36#include <game/client/ui_listbox.h>
37#include <game/client/ui_scrollregion.h>
38#include <game/editor/editor_history.h>
39#include <game/editor/mapitems/image.h>
40#include <game/editor/mapitems/sound.h>
41#include <game/localization.h>
42
43#include <algorithm>
44#include <chrono>
45#include <iterator>
46#include <limits>
47#include <tuple>
48#include <type_traits>
49#include <unordered_map>
50
51static const char *VANILLA_IMAGES[] = {
52 "bg_cloud1",
53 "bg_cloud2",
54 "bg_cloud3",
55 "desert_doodads",
56 "desert_main",
57 "desert_mountains",
58 "desert_mountains2",
59 "desert_sun",
60 "generic_deathtiles",
61 "generic_unhookable",
62 "grass_doodads",
63 "grass_main",
64 "jungle_background",
65 "jungle_deathtiles",
66 "jungle_doodads",
67 "jungle_main",
68 "jungle_midground",
69 "jungle_unhookables",
70 "moon",
71 "mountains",
72 "snow",
73 "stars",
74 "sun",
75 "winter_doodads",
76 "winter_main",
77 "winter_mountains",
78 "winter_mountains2",
79 "winter_mountains3"};
80
81bool CEditor::IsVanillaImage(const char *pImage)
82{
83 return std::any_of(first: std::begin(arr&: VANILLA_IMAGES), last: std::end(arr&: VANILLA_IMAGES), pred: [pImage](const char *pVanillaImage) { return str_comp(a: pImage, b: pVanillaImage) == 0; });
84}
85
86bool CEditor::CallbackOpenMap(const char *pFilename, int StorageType, void *pUser)
87{
88 CEditor *pEditor = (CEditor *)pUser;
89 if(pEditor->Load(pFilename, StorageType))
90 {
91 pEditor->Map()->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && pEditor->m_FileBrowser.IsValidSaveFilename();
92 if(pEditor->m_Dialog == DIALOG_FILE)
93 {
94 pEditor->OnDialogClose();
95 }
96 return true;
97 }
98 else
99 {
100 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFilename);
101 return false;
102 }
103}
104
105bool CEditor::CallbackAppendMap(const char *pFilename, int StorageType, void *pUser)
106{
107 CEditor *pEditor = (CEditor *)pUser;
108 const auto &&ErrorHandler = [pEditor](const char *pErrorMessage) {
109 pEditor->ShowFileDialogError(pFormat: "%s", pErrorMessage);
110 log_error("editor/append", "%s", pErrorMessage);
111 };
112 if(pEditor->Map()->Append(pFilename, StorageType, IgnoreHistory: false, ErrorHandler))
113 {
114 pEditor->OnDialogClose();
115 return true;
116 }
117 else
118 {
119 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFilename);
120 return false;
121 }
122}
123
124bool CEditor::CallbackSaveMap(const char *pFilename, int StorageType, void *pUser)
125{
126 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
127
128 CEditor *pEditor = static_cast<CEditor *>(pUser);
129
130 // Save map to specified file
131 if(pEditor->Save(pFilename))
132 {
133 if(pEditor->Map()->m_aFilename != pFilename)
134 {
135 str_copy(dst&: pEditor->Map()->m_aFilename, src: pFilename);
136 }
137 pEditor->Map()->m_ValidSaveFilename = true;
138 pEditor->Map()->m_Modified = false;
139 pEditor->UpdateMapDisplayNames();
140 }
141 else
142 {
143 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFilename);
144 return false;
145 }
146
147 // Also update autosave if it's older than half the configured autosave interval, so we also have periodic backups.
148 const float Time = pEditor->Client()->GlobalTime();
149 if(g_Config.m_EdAutosaveInterval > 0 && pEditor->Map()->m_LastSaveTime < Time && Time - pEditor->Map()->m_LastSaveTime > 30 * g_Config.m_EdAutosaveInterval)
150 {
151 const auto &&ErrorHandler = [pEditor](const char *pErrorMessage) {
152 pEditor->ShowFileDialogError(pFormat: "%s", pErrorMessage);
153 log_error("editor/autosave", "%s", pErrorMessage);
154 };
155 if(!pEditor->Map()->PerformAutosave(ErrorHandler))
156 return false;
157 }
158
159 pEditor->OnDialogClose();
160 return true;
161}
162
163bool CEditor::CallbackSaveCopyMap(const char *pFilename, int StorageType, void *pUser)
164{
165 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
166
167 CEditor *pEditor = static_cast<CEditor *>(pUser);
168
169 if(pEditor->Save(pFilename))
170 {
171 pEditor->OnDialogClose();
172 return true;
173 }
174 else
175 {
176 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFilename);
177 return false;
178 }
179}
180
181bool CEditor::CallbackSaveImage(const char *pFilename, int StorageType, void *pUser)
182{
183 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
184
185 CEditor *pEditor = static_cast<CEditor *>(pUser);
186
187 std::shared_ptr<CEditorImage> pImg = pEditor->Map()->SelectedImage();
188
189 if(CImageLoader::SavePng(File: pEditor->Storage()->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: StorageType), pFilename, Image: *pImg))
190 {
191 pEditor->OnDialogClose();
192 return true;
193 }
194 else
195 {
196 pEditor->ShowFileDialogError(pFormat: "Failed to write image to file '%s'.", pFilename);
197 return false;
198 }
199}
200
201bool CEditor::CallbackSaveSound(const char *pFilename, int StorageType, void *pUser)
202{
203 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
204
205 CEditor *pEditor = static_cast<CEditor *>(pUser);
206
207 std::shared_ptr<CEditorSound> pSound = pEditor->Map()->SelectedSound();
208
209 IOHANDLE File = pEditor->Storage()->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: StorageType);
210 if(File)
211 {
212 io_write(io: File, buffer: pSound->m_pData, size: pSound->m_DataSize);
213 io_close(io: File);
214 pEditor->OnDialogClose();
215 return true;
216 }
217 pEditor->ShowFileDialogError(pFormat: "Failed to open file '%s'.", pFilename);
218 return false;
219}
220
221bool CEditor::CallbackCustomEntities(const char *pFilename, int StorageType, void *pUser)
222{
223 CEditor *pEditor = (CEditor *)pUser;
224
225 char aBuf[IO_MAX_PATH_LENGTH];
226 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
227
228 if(std::find(first: pEditor->m_vSelectEntitiesFiles.begin(), last: pEditor->m_vSelectEntitiesFiles.end(), val: std::string(aBuf)) != pEditor->m_vSelectEntitiesFiles.end())
229 {
230 pEditor->ShowFileDialogError(pFormat: "Custom entities cannot have the same name as default entities.");
231 return false;
232 }
233
234 CImageInfo ImgInfo;
235 if(!pEditor->Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
236 {
237 pEditor->ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
238 return false;
239 }
240
241 pEditor->m_SelectEntitiesImage = aBuf;
242 pEditor->m_AllowPlaceUnusedTiles = EUnusedEntities::ALLOWED_IMPLICIT;
243 pEditor->m_PreventUnusedTilesWasWarned = false;
244
245 pEditor->Graphics()->UnloadTexture(pIndex: &pEditor->m_EntitiesTexture);
246 pEditor->m_EntitiesTexture = pEditor->Graphics()->LoadTextureRawMove(Image&: ImgInfo, Flags: pEditor->Graphics()->TextureLoadFlags());
247
248 pEditor->OnDialogClose();
249 return true;
250}
251
252void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonId, const void *pStopButtonId, const void *pSeekBarId, int SampleId)
253{
254 CUIRect Button, SeekBar;
255 // play/pause button
256 {
257 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
258 if(DoButton_FontIcon(pId: pPlayPauseButtonId, pText: Sound()->IsPlaying(SampleId) ? FontIcon::PAUSE : FontIcon::PLAY, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Play/pause audio preview.", Corners: IGraphics::CORNER_ALL) ||
259 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_SPACE)))
260 {
261 if(Sound()->IsPlaying(SampleId))
262 {
263 Sound()->Pause(SampleId);
264 }
265 else
266 {
267 if(SampleId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
268 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
269
270 Sound()->Play(ChannelId: CSounds::CHN_GUI, SampleId, Flags: ISound::FLAG_PREVIEW, Volume: 1.0f);
271 }
272 }
273 }
274 // stop button
275 {
276 View.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &View);
277 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
278 if(DoButton_FontIcon(pId: pStopButtonId, pText: FontIcon::STOP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Stop audio preview.", Corners: IGraphics::CORNER_ALL))
279 {
280 Sound()->Stop(SampleId);
281 }
282 }
283 // do seekbar
284 {
285 View.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &View);
286 const float Cut = std::min(a: View.w, b: 200.0f);
287 View.VSplitLeft(Cut, pLeft: &SeekBar, pRight: &View);
288 SeekBar.HMargin(Cut: 2.5f, pOtherRect: &SeekBar);
289
290 const float Rounding = 5.0f;
291
292 char aBuffer[64];
293 const float CurrentTime = Sound()->GetSampleCurrentTime(SampleId);
294 const float TotalTime = Sound()->GetSampleTotalTime(SampleId);
295
296 // draw seek bar
297 SeekBar.Draw(Color: ColorRGBA(0, 0, 0, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
298
299 // draw filled bar
300 const float Amount = CurrentTime / TotalTime;
301 CUIRect FilledBar = SeekBar;
302 FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
303 FilledBar.Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
304
305 // draw time
306 char aCurrentTime[32];
307 str_time_float(secs: CurrentTime, format: ETimeFormat::HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
308 char aTotalTime[32];
309 str_time_float(secs: TotalTime, format: ETimeFormat::HOURS, buffer: aTotalTime, buffer_size: sizeof(aTotalTime));
310 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s / %s", aCurrentTime, aTotalTime);
311 Ui()->DoLabel(pRect: &SeekBar, pText: aBuffer, Size: SeekBar.h * 0.70f, Align: TEXTALIGN_MC);
312
313 // do the logic
314 const bool Inside = Ui()->MouseInside(pRect: &SeekBar);
315
316 if(Ui()->CheckActiveItem(pId: pSeekBarId))
317 {
318 if(!Ui()->MouseButton(Index: 0))
319 {
320 Ui()->SetActiveItem(nullptr);
321 }
322 else
323 {
324 const float AmountSeek = std::clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f);
325 Sound()->SetSampleCurrentTime(SampleId, Time: AmountSeek);
326 }
327 }
328 else if(Ui()->HotItem() == pSeekBarId)
329 {
330 if(Ui()->MouseButton(Index: 0))
331 Ui()->SetActiveItem(pSeekBarId);
332 }
333
334 if(Inside && !Ui()->MouseButton(Index: 0))
335 Ui()->SetHotItem(pSeekBarId);
336 }
337}
338
339void CEditor::DoMapTabs(CUIRect MapTabs)
340{
341 CScrollRegionParams ScrollParams;
342 ScrollParams.m_ScrollbarThickness = 6.0f;
343 ScrollParams.m_ScrollbarMargin = 2.0f;
344 ScrollParams.m_ScrollbarNoOuterMargin = true;
345 ScrollParams.m_ScrollUnit = 140.0f;
346 ScrollParams.m_ScrollHorizontal = true;
347 m_MapTabsScrollRegion.Begin(pClipRect: &MapTabs, pParams: &ScrollParams);
348
349 size_t Index = 0;
350 std::optional<size_t> CloseIndex = std::nullopt;
351 for(const auto &pMap : m_vpMaps)
352 {
353 const float ButtonWidth = std::clamp(val: TextRender()->TextWidth(Size: 10.0f, pText: pMap->m_aDisplayName) + 10.0f, lo: 60.0f, hi: 120.0f);
354
355 CUIRect MapButton;
356 MapTabs.VSplitLeft(Cut: ButtonWidth + 18.0f, pLeft: &MapButton, pRight: &MapTabs);
357 MapTabs.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &MapTabs);
358 if(m_MapTabsScrollRegion.AddRect(Rect: MapButton, ShouldScrollHere: m_MapTabsRevealSelected && m_SelectedMap == Index))
359 {
360 CUIRect CloseButton;
361 MapButton.VSplitRight(Cut: 18.0f, pLeft: &MapButton, pRight: &CloseButton);
362
363 const bool Saving = IsSaving(pFilename: pMap->m_aFilename);
364
365 char aTooltip[256];
366 str_format(buffer: aTooltip, buffer_size: sizeof(aTooltip), format: "Select map '%s'.", pMap->m_aFilename[0] == '\0' ? "unnamed" : pMap->m_aFilename);
367 const int TabButtonResult = DoButton_Ex(pId: &pMap->m_TabSelectButtonId,
368 pText: pMap->m_aDisplayName, Checked: m_SelectedMap == Index ? 1 : 0, pRect: &MapButton, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT | (Saving ? 0 : (int)BUTTONFLAG_MIDDLE),
369 pToolTip: aTooltip, Corners: IGraphics::CORNER_L);
370
371 int CloseButtonResult;
372 if(Saving)
373 {
374 CloseButtonResult = DoButton_Ex(pId: &pMap->m_TabCloseButtonId, pText: "", Checked: 0, pRect: &CloseButton, Flags: BUTTONFLAG_RIGHT, pToolTip: "This map is being saved.", Corners: IGraphics::CORNER_R);
375 Ui()->RenderProgressSpinner(Center: CloseButton.Center(), OuterRadius: 4.0f);
376 }
377 else
378 {
379 const bool ShowCloseIcon = !pMap->m_Modified || Ui()->HotItem() == &pMap->m_TabCloseButtonId;
380 CloseButtonResult = DoButton_FontIcon(pId: &pMap->m_TabCloseButtonId,
381 pText: ShowCloseIcon ? FontIcon::XMARK : FontIcon::CIRCLE, Checked: 0, pRect: &CloseButton, Flags: BUTTONFLAG_ALL,
382 pToolTip: pMap->m_Modified ? "Close the selected map. This map has unsaved changes." : "Close the selected map.", Corners: IGraphics::CORNER_R, FontSize: ShowCloseIcon ? 9.0f : 6.0f);
383 }
384
385 if(TabButtonResult == 1)
386 {
387 m_SelectedMap = Index;
388 m_MapTabsScrollRegion.ScrollHere();
389 Reset();
390 }
391 else if(TabButtonResult == 2 || CloseButtonResult == 2)
392 {
393 m_PopupMapTab.m_pEditor = this;
394 m_PopupMapTab.m_SelectedMap = Index;
395 Ui()->DoPopupMenu(pId: &m_PopupMapTab, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120.0f, Height: 80.0f, pContext: &m_PopupMapTab, pfnFunc: CPopupMapTab::Render);
396 }
397 else if(TabButtonResult == 3 || CloseButtonResult == 1 || CloseButtonResult == 3)
398 {
399 CloseIndex = Index;
400 }
401 }
402
403 ++Index;
404 }
405
406 m_MapTabsRevealSelected = false;
407 m_MapTabsScrollRegion.End();
408
409 if(CloseIndex.has_value())
410 {
411 CloseMap(Index: CloseIndex.value(), Confirm: true);
412 }
413
414 if(m_Dialog == DIALOG_NONE &&
415 CLineInput::GetActiveInput() == nullptr &&
416 Ui()->CheckActiveItem(pId: nullptr))
417 {
418 // Close selected map with Ctrl+F4
419 if(!CloseIndex.has_value() &&
420 Input()->ModifierIsPressed() &&
421 Input()->KeyPress(Key: KEY_F4))
422 {
423 CloseMap(Index: m_SelectedMap, Confirm: true);
424 }
425
426 // Switch between maps with Ctrl+Tab and Ctrl+Shift+Tab
427 if(m_vpMaps.size() > 1 &&
428 Input()->ModifierIsPressed() &&
429 Input()->KeyPress(Key: KEY_TAB))
430 {
431 if(Input()->ShiftIsPressed())
432 {
433 m_SelectedMap = m_SelectedMap == 0 ? m_vpMaps.size() - 1 : m_SelectedMap - 1;
434 }
435 else
436 {
437 m_SelectedMap = m_SelectedMap == m_vpMaps.size() - 1 ? 0 : m_SelectedMap + 1;
438 }
439 m_MapTabsRevealSelected = true;
440 Reset();
441 }
442 }
443}
444
445void CEditor::DoToolbarLayers(CUIRect ToolBar)
446{
447 const bool ModPressed = Input()->ModifierIsPressed();
448 const bool ShiftPressed = Input()->ShiftIsPressed();
449
450 // handle shortcut for info button
451 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && !ShiftPressed)
452 {
453 if(m_ShowTileInfo == SHOW_TILE_HEXADECIMAL)
454 m_ShowTileInfo = SHOW_TILE_DECIMAL;
455 else if(m_ShowTileInfo != SHOW_TILE_OFF)
456 m_ShowTileInfo = SHOW_TILE_OFF;
457 else
458 m_ShowTileInfo = SHOW_TILE_DECIMAL;
459 }
460
461 // handle shortcut for hex button
462 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && ShiftPressed)
463 {
464 m_ShowTileInfo = m_ShowTileInfo == SHOW_TILE_HEXADECIMAL ? SHOW_TILE_OFF : SHOW_TILE_HEXADECIMAL;
465 }
466
467 // handle shortcut for unused button
468 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_U) && ModPressed && m_AllowPlaceUnusedTiles != EUnusedEntities::ALLOWED_IMPLICIT)
469 {
470 if(m_AllowPlaceUnusedTiles == EUnusedEntities::ALLOWED_EXPLICIT)
471 {
472 m_AllowPlaceUnusedTiles = EUnusedEntities::NOT_ALLOWED;
473 }
474 else
475 {
476 m_AllowPlaceUnusedTiles = EUnusedEntities::ALLOWED_EXPLICIT;
477 }
478 }
479
480 CUIRect ToolbarTop, ToolbarBottom;
481 CUIRect Button;
482
483 ToolBar.HSplitMid(pTop: &ToolbarTop, pBottom: &ToolbarBottom, Spacing: 5.0f);
484
485 // top line buttons
486 {
487 // detail button
488 ToolbarTop.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolbarTop);
489 static int s_HqButton = 0;
490 if(DoButton_Editor(pId: &s_HqButton, pText: "HD", Checked: Map()->m_ShowDetail, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+H] Toggle high detail.") ||
491 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_H) && ModPressed))
492 {
493 Map()->m_ShowDetail = !Map()->m_ShowDetail;
494 }
495
496 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
497
498 // animation buttons
499 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
500 static char s_JumpStartButton = 0;
501 if(DoButton_FontIcon(pId: &s_JumpStartButton, pText: FontIcon::BACKWARD_STEP, Checked: false, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Jump to beginning of animation.", Corners: IGraphics::CORNER_L))
502 {
503 Map()->m_EnvelopeEvaluator.m_AnimateTime = 0;
504 Map()->m_EnvelopeEvaluator.m_Animate = false;
505 }
506
507 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
508 static char s_AnimateButton = 0;
509 if(DoButton_FontIcon(pId: &s_AnimateButton, pText: Map()->m_EnvelopeEvaluator.m_Animate ? FontIcon::PAUSE : FontIcon::PLAY, Checked: Map()->m_EnvelopeEvaluator.m_Animate, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+M] Toggle animation.", Corners: IGraphics::CORNER_NONE) ||
510 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_M) && ModPressed))
511 {
512 Map()->m_EnvelopeEvaluator.m_AnimateStart = Client()->GlobalTime() - Map()->m_EnvelopeEvaluator.m_AnimateTime;
513 Map()->m_EnvelopeEvaluator.m_Animate = !Map()->m_EnvelopeEvaluator.m_Animate;
514 }
515
516 // animation settings button
517 ToolbarTop.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &ToolbarTop);
518 static char s_AnimateSettingsButton;
519 if(DoButton_FontIcon(pId: &s_AnimateSettingsButton, pText: FontIcon::CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Change the animation settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
520 {
521 Map()->m_EnvelopeEvaluator.m_AnimateUpdatePopup = true;
522 static SPopupMenuId s_PopupAnimateSettingsId;
523 Ui()->DoPopupMenu(pId: &s_PopupAnimateSettingsId, X: Button.x, Y: Button.y + Button.h, Width: 150.0f, Height: 37.0f, pContext: this, pfnFunc: PopupAnimateSettings);
524 }
525
526 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
527
528 // proof button
529 ToolbarTop.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolbarTop);
530 if(DoButton_Ex(pId: &m_QuickActionProof, pText: m_QuickActionProof.Label(), Checked: m_QuickActionProof.Active(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionProof.Description(), Corners: IGraphics::CORNER_L))
531 {
532 m_QuickActionProof.Call();
533 }
534
535 ToolbarTop.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &ToolbarTop);
536 static int s_ProofModeButton = 0;
537 if(DoButton_FontIcon(pId: &s_ProofModeButton, pText: FontIcon::CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Select proof mode.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
538 {
539 static SPopupMenuId s_PopupProofModeId;
540 Ui()->DoPopupMenu(pId: &s_PopupProofModeId, X: Button.x, Y: Button.y + Button.h, Width: 60.0f, Height: 36.0f, pContext: this, pfnFunc: PopupProofMode);
541 }
542
543 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
544
545 // zoom button
546 ToolbarTop.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolbarTop);
547 static int s_ZoomButton = 0;
548 if(DoButton_Editor(pId: &s_ZoomButton, pText: "Zoom", Checked: Map()->m_PreviewZoom, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Toggle preview of how layers will be zoomed ingame."))
549 {
550 Map()->m_PreviewZoom = !Map()->m_PreviewZoom;
551 }
552
553 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
554
555 // grid button
556 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
557 static int s_GridButton = 0;
558 if(DoButton_FontIcon(pId: &s_GridButton, pText: FontIcon::BORDER_ALL, Checked: m_QuickActionToggleGrid.Active(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionToggleGrid.Description(), Corners: IGraphics::CORNER_L) ||
559 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_G) && ModPressed && !ShiftPressed))
560 {
561 m_QuickActionToggleGrid.Call();
562 }
563
564 // grid settings button
565 ToolbarTop.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &ToolbarTop);
566 static char s_GridSettingsButton;
567 if(DoButton_FontIcon(pId: &s_GridSettingsButton, pText: FontIcon::CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Change the grid settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
568 {
569 MapView()->MapGrid()->DoSettingsPopup(Position: vec2(Button.x, Button.y + Button.h));
570 }
571
572 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
573
574 // zoom group
575 ToolbarTop.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &ToolbarTop);
576 static int s_ZoomOutButton = 0;
577 if(DoButton_FontIcon(pId: &s_ZoomOutButton, pText: FontIcon::MINUS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionZoomOut.Description(), Corners: IGraphics::CORNER_L))
578 {
579 m_QuickActionZoomOut.Call();
580 }
581
582 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
583 static int s_ZoomNormalButton = 0;
584 if(DoButton_FontIcon(pId: &s_ZoomNormalButton, pText: FontIcon::MAGNIFYING_GLASS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionResetZoom.Description(), Corners: IGraphics::CORNER_NONE))
585 {
586 m_QuickActionResetZoom.Call();
587 }
588
589 ToolbarTop.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &ToolbarTop);
590 static int s_ZoomInButton = 0;
591 if(DoButton_FontIcon(pId: &s_ZoomInButton, pText: FontIcon::PLUS, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionZoomIn.Description(), Corners: IGraphics::CORNER_R))
592 {
593 m_QuickActionZoomIn.Call();
594 }
595
596 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
597
598 // undo/redo group
599 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
600 static int s_UndoButton = 0;
601 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FontIcon::UNDO, Checked: Map()->m_EditorHistory.CanUndo() - 1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Z] Undo the last action.", Corners: IGraphics::CORNER_L))
602 {
603 Map()->m_EditorHistory.Undo();
604 }
605
606 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
607 static int s_RedoButton = 0;
608 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FontIcon::REDO, Checked: Map()->m_EditorHistory.CanRedo() - 1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Y] Redo the last action.", Corners: IGraphics::CORNER_R))
609 {
610 Map()->m_EditorHistory.Redo();
611 }
612
613 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
614
615 // brush manipulation
616 {
617 int Enabled = m_pBrush->IsEmpty() ? -1 : 0;
618
619 // flip buttons
620 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
621 static int s_FlipXButton = 0;
622 if(DoButton_FontIcon(pId: &s_FlipXButton, pText: FontIcon::ARROWS_LEFT_RIGHT, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[N] Flip the brush horizontally.", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_N) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
623 {
624 for(auto &pLayer : m_pBrush->m_vpLayers)
625 pLayer->BrushFlipX();
626 }
627
628 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
629 static int s_FlipyButton = 0;
630 if(DoButton_FontIcon(pId: &s_FlipyButton, pText: FontIcon::ARROWS_UP_DOWN, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[M] Flip the brush vertically.", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_M) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
631 {
632 for(auto &pLayer : m_pBrush->m_vpLayers)
633 pLayer->BrushFlipY();
634 }
635 ToolbarTop.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarTop);
636
637 // rotate buttons
638 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
639 static int s_RotationAmount = 90;
640 bool TileLayer = false;
641 // check for tile layers in brush selection
642 for(auto &pLayer : m_pBrush->m_vpLayers)
643 if(pLayer->m_Type == LAYERTYPE_TILES)
644 {
645 TileLayer = true;
646 s_RotationAmount = std::max(a: 90, b: (s_RotationAmount / 90) * 90);
647 break;
648 }
649
650 static int s_CcwButton = 0;
651 if(DoButton_FontIcon(pId: &s_CcwButton, pText: FontIcon::ARROW_ROTATE_LEFT, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[R] Rotate the brush counter-clockwise.", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_R) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
652 {
653 for(auto &pLayer : m_pBrush->m_vpLayers)
654 pLayer->BrushRotate(Amount: -s_RotationAmount / 360.0f * pi * 2);
655 }
656
657 ToolbarTop.VSplitLeft(Cut: 30.0f, pLeft: &Button, pRight: &ToolbarTop);
658 auto RotationAmountRes = UiDoValueSelector(pId: &s_RotationAmount, pRect: &Button, pLabel: "", Current: s_RotationAmount, Min: TileLayer ? 90 : 1, Max: 359, Step: TileLayer ? 90 : 1, Scale: TileLayer ? 10.0f : 2.0f, pToolTip: "Rotation of the brush in degrees. Use left mouse button to drag and change the value. Hold shift to be more precise.", IsDegree: true, IsHex: false, Corners: IGraphics::CORNER_NONE);
659 s_RotationAmount = RotationAmountRes.m_Value;
660
661 ToolbarTop.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarTop);
662 static int s_CwButton = 0;
663 if(DoButton_FontIcon(pId: &s_CwButton, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: Enabled, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[T] Rotate the brush clockwise.", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_T) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen()))
664 {
665 for(auto &pLayer : m_pBrush->m_vpLayers)
666 pLayer->BrushRotate(Amount: s_RotationAmount / 360.0f * pi * 2);
667 }
668 }
669
670 // Color pipette and palette
671 {
672 const float PipetteButtonWidth = 30.0f;
673 const float ColorPickerButtonWidth = 20.0f;
674 const float Spacing = 2.0f;
675 const size_t NumColorsShown = std::clamp<int>(val: round_to_int(f: (ToolbarTop.w - PipetteButtonWidth - 40.0f) / (ColorPickerButtonWidth + Spacing)), lo: 1, hi: std::size(m_aSavedColors));
676
677 CUIRect ColorPalette;
678 ToolbarTop.VSplitRight(Cut: NumColorsShown * (ColorPickerButtonWidth + Spacing) + PipetteButtonWidth, pLeft: &ToolbarTop, pRight: &ColorPalette);
679
680 // Pipette button
681 static char s_PipetteButton;
682 ColorPalette.VSplitLeft(Cut: PipetteButtonWidth, pLeft: &Button, pRight: &ColorPalette);
683 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
684 if(DoButton_FontIcon(pId: &s_PipetteButton, pText: FontIcon::EYE_DROPPER, Checked: m_QuickActionPipette.Active(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionPipette.Description(), Corners: IGraphics::CORNER_ALL) ||
685 (CLineInput::GetActiveInput() == nullptr && ModPressed && ShiftPressed && Input()->KeyPress(Key: KEY_C)))
686 {
687 m_QuickActionPipette.Call();
688 }
689
690 // Palette color pickers
691 for(size_t i = 0; i < NumColorsShown; ++i)
692 {
693 ColorPalette.VSplitLeft(Cut: ColorPickerButtonWidth, pLeft: &Button, pRight: &ColorPalette);
694 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
695 const auto &&SetColor = [&](ColorRGBA NewColor) {
696 m_aSavedColors[i] = NewColor;
697 };
698 DoColorPickerButton(pId: &m_aSavedColors[i], pRect: &Button, Color: m_aSavedColors[i], SetColor);
699 }
700 }
701 }
702
703 // Bottom line buttons
704 {
705 // refocus button
706 {
707 ToolbarBottom.VSplitLeft(Cut: 50.0f, pLeft: &Button, pRight: &ToolbarBottom);
708 int FocusButtonChecked = MapView()->IsFocused() ? -1 : 1;
709 if(DoButton_Editor(pId: &m_QuickActionRefocus, pText: m_QuickActionRefocus.Label(), Checked: FocusButtonChecked, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionRefocus.Description()) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_HOME)))
710 m_QuickActionRefocus.Call();
711 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarBottom);
712 }
713
714 // brush picker button
715 {
716 ToolbarBottom.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &ToolbarBottom);
717 const int Checked = m_QuickActionBrushPicker.Disabled() ? -1 : (m_ShowPicker ? 1 : 0);
718 if(DoButton_FontIcon(pId: &m_QuickActionBrushPicker, pText: FontIcon::BRUSH, Checked, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionBrushPicker.Description(), Corners: IGraphics::CORNER_ALL))
719 {
720 m_QuickActionBrushPicker.Call();
721 }
722 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarBottom);
723 }
724
725 // tile manipulation
726 {
727 // do tele/tune/switch/speedup button
728 {
729 std::shared_ptr<CLayerTiles> pS = std::static_pointer_cast<CLayerTiles>(r: Map()->SelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
730 if(pS)
731 {
732 const char *pButtonName = nullptr;
733 CUi::FPopupMenuFunction pfnPopupFunc = nullptr;
734 int Rows = 0;
735 int ExtraWidth = 0;
736 if(pS == Map()->m_pSwitchLayer)
737 {
738 pButtonName = "Switch";
739 pfnPopupFunc = PopupSwitch;
740 Rows = 3;
741 }
742 else if(pS == Map()->m_pSpeedupLayer)
743 {
744 pButtonName = "Speedup";
745 pfnPopupFunc = PopupSpeedup;
746 Rows = 3;
747 }
748 else if(pS == Map()->m_pTuneLayer)
749 {
750 pButtonName = "Tune";
751 pfnPopupFunc = PopupTune;
752 Rows = 2;
753 }
754 else if(pS == Map()->m_pTeleLayer)
755 {
756 pButtonName = "Tele";
757 pfnPopupFunc = PopupTele;
758 Rows = 3;
759 ExtraWidth = 50;
760 }
761
762 if(pButtonName != nullptr)
763 {
764 static char s_aButtonTooltip[64];
765 str_format(buffer: s_aButtonTooltip, buffer_size: sizeof(s_aButtonTooltip), format: "[Ctrl+T] %s", pButtonName);
766
767 ToolbarBottom.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &ToolbarBottom);
768 static int s_ModifierButton = 0;
769 if(DoButton_Ex(pId: &s_ModifierButton, pText: pButtonName, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: s_aButtonTooltip, Corners: IGraphics::CORNER_ALL) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && ModPressed && Input()->KeyPress(Key: KEY_T)))
770 {
771 static SPopupMenuId s_PopupModifierId;
772 if(!Ui()->IsPopupOpen(pId: &s_PopupModifierId))
773 {
774 Ui()->DoPopupMenu(pId: &s_PopupModifierId, X: Button.x, Y: Button.y + Button.h, Width: 120 + ExtraWidth, Height: 10.0f + Rows * 13.0f, pContext: this, pfnFunc: pfnPopupFunc);
775 }
776 }
777 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &ToolbarBottom);
778 }
779 }
780 }
781 }
782
783 // do add quad/sound button
784 std::shared_ptr<CLayer> pLayer = Map()->SelectedLayer(Index: 0);
785 if(pLayer && (pLayer->m_Type == LAYERTYPE_QUADS || pLayer->m_Type == LAYERTYPE_SOUNDS))
786 {
787 // "Add sound source" button needs more space or the font size will be scaled down
788 ToolbarBottom.VSplitLeft(Cut: (pLayer->m_Type == LAYERTYPE_QUADS) ? 60.0f : 100.0f, pLeft: &Button, pRight: &ToolbarBottom);
789
790 if(pLayer->m_Type == LAYERTYPE_QUADS)
791 {
792 if(DoButton_Editor(pId: &m_QuickActionAddQuad, pText: m_QuickActionAddQuad.Label(), Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddQuad.Description()) ||
793 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed))
794 {
795 m_QuickActionAddQuad.Call();
796 }
797 }
798 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
799 {
800 if(DoButton_Editor(pId: &m_QuickActionAddSoundSource, pText: m_QuickActionAddSoundSource.Label(), Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddSoundSource.Description()) ||
801 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed))
802 {
803 m_QuickActionAddSoundSource.Call();
804 }
805 }
806
807 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &ToolbarBottom);
808 }
809
810 // Brush draw mode button
811 {
812 ToolbarBottom.VSplitLeft(Cut: 65.0f, pLeft: &Button, pRight: &ToolbarBottom);
813 static int s_BrushDrawModeButton = 0;
814 if(DoButton_Editor(pId: &s_BrushDrawModeButton, pText: "Destructive", Checked: m_BrushDrawDestructive, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+D] Toggle brush draw mode: preserve or override existing tiles.") ||
815 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_D) && ModPressed && !ShiftPressed))
816 m_BrushDrawDestructive = !m_BrushDrawDestructive;
817 ToolbarBottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &ToolbarBottom);
818 }
819 }
820}
821
822void CEditor::DoToolbarImages(CUIRect ToolBar)
823{
824 CUIRect ToolBarTop, ToolBarBottom;
825 ToolBar.HSplitMid(pTop: &ToolBarTop, pBottom: &ToolBarBottom, Spacing: 5.0f);
826
827 std::shared_ptr<CEditorImage> pSelectedImage = Map()->SelectedImage();
828 if(pSelectedImage != nullptr)
829 {
830 char aLabel[64];
831 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "Size: %" PRIzu " × %" PRIzu, pSelectedImage->m_Width, pSelectedImage->m_Height);
832 Ui()->DoLabel(pRect: &ToolBarBottom, pText: aLabel, Size: 12.0f, Align: TEXTALIGN_ML);
833 }
834}
835
836void CEditor::DoToolbarSounds(CUIRect ToolBar)
837{
838 CUIRect ToolBarTop, ToolBarBottom;
839 ToolBar.HSplitMid(pTop: &ToolBarTop, pBottom: &ToolBarBottom, Spacing: 5.0f);
840
841 std::shared_ptr<CEditorSound> pSelectedSound = Map()->SelectedSound();
842 if(pSelectedSound != nullptr)
843 {
844 if(pSelectedSound->m_SoundId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
845 Sound()->Stop(SampleId: m_ToolbarPreviewSound);
846 m_ToolbarPreviewSound = pSelectedSound->m_SoundId;
847 }
848 else
849 {
850 m_ToolbarPreviewSound = -1;
851 }
852
853 if(m_ToolbarPreviewSound >= 0)
854 {
855 static int s_PlayPauseButton, s_StopButton, s_SeekBar = 0;
856 DoAudioPreview(View: ToolBarBottom, pPlayPauseButtonId: &s_PlayPauseButton, pStopButtonId: &s_StopButton, pSeekBarId: &s_SeekBar, SampleId: m_ToolbarPreviewSound);
857 }
858}
859
860static void Rotate(const CPoint *pCenter, CPoint *pPoint, float Rotation)
861{
862 int x = pPoint->x - pCenter->x;
863 int y = pPoint->y - pCenter->y;
864 pPoint->x = (int)(x * std::cos(x: Rotation) - y * std::sin(x: Rotation) + pCenter->x);
865 pPoint->y = (int)(x * std::sin(x: Rotation) + y * std::cos(x: Rotation) + pCenter->y);
866}
867
868void CEditor::DoSoundSource(int LayerIndex, CSoundSource *pSource, int Index)
869{
870 static ESoundSourceOp s_Operation = ESoundSourceOp::NONE;
871
872 float CenterX = fx2f(v: pSource->m_Position.x);
873 float CenterY = fx2f(v: pSource->m_Position.y);
874
875 const bool IgnoreGrid = Input()->AltIsPressed();
876
877 if(s_Operation == ESoundSourceOp::NONE)
878 {
879 if(!Ui()->MouseButton(Index: 0))
880 Map()->m_SoundSourceOperationTracker.End();
881 }
882
883 if(Ui()->CheckActiveItem(pId: pSource))
884 {
885 if(s_Operation != ESoundSourceOp::NONE)
886 {
887 Map()->m_SoundSourceOperationTracker.Begin(pSource, Operation: s_Operation, LayerIndex);
888 }
889
890 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
891 {
892 if(s_Operation == ESoundSourceOp::MOVE)
893 {
894 vec2 Pos = MapView()->MouseWorldPos();
895 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
896 {
897 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
898 }
899 pSource->m_Position.x = f2fx(v: Pos.x);
900 pSource->m_Position.y = f2fx(v: Pos.y);
901 }
902 }
903
904 if(s_Operation == ESoundSourceOp::CONTEXT_MENU)
905 {
906 if(!Ui()->MouseButton(Index: 1))
907 {
908 if(Map()->m_vSelectedLayers.size() == 1)
909 {
910 static SPopupMenuId s_PopupSourceId;
911 Ui()->DoPopupMenu(pId: &s_PopupSourceId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 200, pContext: this, pfnFunc: PopupSource);
912 Ui()->DisableMouseLock();
913 }
914 s_Operation = ESoundSourceOp::NONE;
915 Ui()->SetActiveItem(nullptr);
916 }
917 }
918 else
919 {
920 if(!Ui()->MouseButton(Index: 0))
921 {
922 Ui()->DisableMouseLock();
923 s_Operation = ESoundSourceOp::NONE;
924 Ui()->SetActiveItem(nullptr);
925 }
926 }
927
928 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
929 }
930 else if(Ui()->HotItem() == pSource)
931 {
932 m_pUiGotContext = pSource;
933
934 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
935 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold alt to ignore grid.");
936
937 if(Ui()->MouseButton(Index: 0))
938 {
939 s_Operation = ESoundSourceOp::MOVE;
940
941 Ui()->SetActiveItem(pSource);
942 Map()->m_SelectedSoundSource = Index;
943 }
944
945 if(Ui()->MouseButton(Index: 1))
946 {
947 Map()->m_SelectedSoundSource = Index;
948 s_Operation = ESoundSourceOp::CONTEXT_MENU;
949 Ui()->SetActiveItem(pSource);
950 }
951 }
952 else
953 {
954 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
955 }
956
957 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
958 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
959}
960
961void CEditor::UpdateHotSoundSource(const CLayerSounds *pLayer)
962{
963 const vec2 MouseWorld = MapView()->MouseWorldPos();
964
965 float MinDist = 500.0f;
966 const void *pMinSourceId = nullptr;
967
968 const auto UpdateMinimum = [&](vec2 Position, const void *pId) {
969 const float CurrDist = distance_squared(a: Position, b: MouseWorld) / (MapView()->MouseWorldScale() * MapView()->MouseWorldScale());
970 if(CurrDist < MinDist)
971 {
972 MinDist = CurrDist;
973 pMinSourceId = pId;
974 }
975 };
976
977 for(const CSoundSource &Source : pLayer->m_vSources)
978 {
979 UpdateMinimum(vec2(fx2f(v: Source.m_Position.x), fx2f(v: Source.m_Position.y)), &Source);
980 }
981
982 if(pMinSourceId != nullptr)
983 {
984 Ui()->SetHotItem(pMinSourceId);
985 }
986}
987
988void CEditor::PreparePointDrag(const CQuad *pQuad, int QuadIndex, int PointIndex)
989{
990 m_QuadDragOriginalPoints[QuadIndex][PointIndex] = pQuad->m_aPoints[PointIndex];
991}
992
993void CEditor::DoPointDrag(CQuad *pQuad, int QuadIndex, int PointIndex, ivec2 Offset)
994{
995 pQuad->m_aPoints[PointIndex] = m_QuadDragOriginalPoints[QuadIndex][PointIndex] + Offset;
996}
997
998CEditor::EAxis CEditor::GetDragAxis(ivec2 Offset) const
999{
1000 if(Input()->ShiftIsPressed())
1001 if(absolute(a: Offset.x) < absolute(a: Offset.y))
1002 return EAxis::Y;
1003 else
1004 return EAxis::X;
1005 else
1006 return EAxis::NONE;
1007}
1008
1009void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) const
1010{
1011 if(Axis == EAxis::NONE)
1012 return;
1013
1014 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1015 if(Axis == EAxis::X)
1016 {
1017 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x + Point.x) / 2.0f, fx2f(v: OriginalPoint.y), fx2f(v: Point.x - OriginalPoint.x), 1.0f * MapView()->MouseWorldScale());
1018 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
1019 }
1020 else if(Axis == EAxis::Y)
1021 {
1022 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y + Point.y) / 2.0f, 1.0f * MapView()->MouseWorldScale(), fx2f(v: Point.y - OriginalPoint.y));
1023 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
1024 }
1025
1026 // Draw ghost of original point
1027 IGraphics::CQuadItem QuadItem(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y), 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
1028 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1029}
1030
1031void CEditor::ComputePointAlignments(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, ivec2 Offset, std::vector<SAlignmentInfo> &vAlignments, bool Append) const
1032{
1033 if(!Append)
1034 vAlignments.clear();
1035 if(!g_Config.m_EdAlignQuads)
1036 return;
1037
1038 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
1039
1040 // Perform computation from the original position of this point
1041 int Threshold = f2fx(v: std::max(a: 5.0f, b: 10.0f * MapView()->MouseWorldScale()));
1042 CPoint OrigPoint = m_QuadDragOriginalPoints.at(k: QuadIndex)[PointIndex];
1043 // Get the "current" point by applying the offset
1044 CPoint Point = OrigPoint + Offset;
1045
1046 // Save smallest diff on both axis to only keep closest alignments
1047 ivec2 SmallestDiff = ivec2(Threshold + 1, Threshold + 1);
1048 // Store both axis alignments in separate vectors
1049 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1050
1051 // Check if we can align/snap to a specific point
1052 auto &&CheckAlignment = [&](CPoint *pQuadPoint) {
1053 ivec2 DirectedDiff = *pQuadPoint - Point;
1054 ivec2 Diff = ivec2(absolute(a: DirectedDiff.x), absolute(a: DirectedDiff.y));
1055
1056 if(Diff.x <= Threshold && (!GridEnabled || Diff.x == 0))
1057 {
1058 // Only store alignments that have the smallest difference
1059 if(Diff.x < SmallestDiff.x)
1060 {
1061 vAlignmentsX.clear();
1062 SmallestDiff.x = Diff.x;
1063 }
1064
1065 // We can have multiple alignments having the same difference/distance
1066 if(Diff.x == SmallestDiff.x)
1067 {
1068 vAlignmentsX.push_back(x: SAlignmentInfo{
1069 .m_AlignedPoint: *pQuadPoint, // Aligned point
1070 {.m_X: OrigPoint.y}, // Value that can change (which is not snapped), original position
1071 .m_Axis: EAxis::Y, // The alignment axis
1072 .m_PointIndex: PointIndex, // The index of the point
1073 .m_Diff: DirectedDiff.x,
1074 });
1075 }
1076 }
1077
1078 if(Diff.y <= Threshold && (!GridEnabled || Diff.y == 0))
1079 {
1080 // Only store alignments that have the smallest difference
1081 if(Diff.y < SmallestDiff.y)
1082 {
1083 vAlignmentsY.clear();
1084 SmallestDiff.y = Diff.y;
1085 }
1086
1087 if(Diff.y == SmallestDiff.y)
1088 {
1089 vAlignmentsY.push_back(x: SAlignmentInfo{
1090 .m_AlignedPoint: *pQuadPoint,
1091 {.m_X: OrigPoint.x},
1092 .m_Axis: EAxis::X,
1093 .m_PointIndex: PointIndex,
1094 .m_Diff: DirectedDiff.y,
1095 });
1096 }
1097 }
1098 };
1099
1100 // Iterate through all the quads of the current layer
1101 // Check alignment with each point of the quad (corners & pivot)
1102 // Compute an AABB (Axis Aligned Bounding Box) to get the center of the quad
1103 // Check alignment with the center of the quad
1104 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
1105 {
1106 auto *pCurrentQuad = &pLayer->m_vQuads[i];
1107 CPoint Min = pCurrentQuad->m_aPoints[0];
1108 CPoint Max = pCurrentQuad->m_aPoints[0];
1109
1110 for(int v = 0; v < 5; v++)
1111 {
1112 CPoint *pQuadPoint = &pCurrentQuad->m_aPoints[v];
1113
1114 if(v != 4)
1115 { // Don't use pivot to compute AABB
1116 if(pQuadPoint->x < Min.x)
1117 Min.x = pQuadPoint->x;
1118 if(pQuadPoint->y < Min.y)
1119 Min.y = pQuadPoint->y;
1120 if(pQuadPoint->x > Max.x)
1121 Max.x = pQuadPoint->x;
1122 if(pQuadPoint->y > Max.y)
1123 Max.y = pQuadPoint->y;
1124 }
1125
1126 // Don't check alignment with current point
1127 if(pQuadPoint == &pQuad->m_aPoints[PointIndex])
1128 continue;
1129
1130 // Don't check alignment with other selected points
1131 bool IsCurrentPointSelected = Map()->IsQuadSelected(Index: i) && (Map()->IsQuadCornerSelected(Index: v) || (v == PointIndex && PointIndex == 4));
1132 if(IsCurrentPointSelected)
1133 continue;
1134
1135 CheckAlignment(pQuadPoint);
1136 }
1137
1138 // Don't check alignment with center of selected quads
1139 if(!Map()->IsQuadSelected(Index: i))
1140 {
1141 CPoint Center = (Min + Max) / 2.0f;
1142 CheckAlignment(&Center);
1143 }
1144 }
1145
1146 // Finally concatenate both alignment vectors into the output
1147 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1148 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1149 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1150}
1151
1152void CEditor::ComputePointsAlignments(const std::shared_ptr<CLayerQuads> &pLayer, bool Pivot, ivec2 Offset, std::vector<SAlignmentInfo> &vAlignments) const
1153{
1154 // This method is used to compute alignments from selected points
1155 // and only apply the closest alignment on X and Y to the offset.
1156
1157 vAlignments.clear();
1158 std::vector<SAlignmentInfo> vAllAlignments;
1159
1160 for(int Selected : Map()->m_vSelectedQuads)
1161 {
1162 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1163
1164 if(!Pivot)
1165 {
1166 for(int m = 0; m < 4; m++)
1167 {
1168 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1169 {
1170 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: m, Offset, vAlignments&: vAllAlignments, Append: true);
1171 }
1172 }
1173 }
1174 else
1175 {
1176 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: 4, Offset, vAlignments&: vAllAlignments, Append: true);
1177 }
1178 }
1179
1180 ivec2 SmallestDiff = ivec2(std::numeric_limits<int>::max(), std::numeric_limits<int>::max());
1181 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1182
1183 for(const auto &Alignment : vAllAlignments)
1184 {
1185 int AbsDiff = absolute(a: Alignment.m_Diff);
1186 if(Alignment.m_Axis == EAxis::X)
1187 {
1188 if(AbsDiff < SmallestDiff.y)
1189 {
1190 SmallestDiff.y = AbsDiff;
1191 vAlignmentsY.clear();
1192 }
1193 if(AbsDiff == SmallestDiff.y)
1194 vAlignmentsY.emplace_back(args: Alignment);
1195 }
1196 else if(Alignment.m_Axis == EAxis::Y)
1197 {
1198 if(AbsDiff < SmallestDiff.x)
1199 {
1200 SmallestDiff.x = AbsDiff;
1201 vAlignmentsX.clear();
1202 }
1203 if(AbsDiff == SmallestDiff.x)
1204 vAlignmentsX.emplace_back(args: Alignment);
1205 }
1206 }
1207
1208 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1209 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1210 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1211}
1212
1213void CEditor::ComputeAABBAlignments(const std::shared_ptr<CLayerQuads> &pLayer, const SAxisAlignedBoundingBox &AABB, ivec2 Offset, std::vector<SAlignmentInfo> &vAlignments) const
1214{
1215 vAlignments.clear();
1216 if(!g_Config.m_EdAlignQuads)
1217 return;
1218
1219 // This method is a bit different than the point alignment in the way where instead of trying to align 1 point to all quads,
1220 // we try to align 5 points to all quads, these 5 points being 5 points of an AABB.
1221 // Otherwise, the concept is the same, we use the original position of the AABB to make the computations.
1222 int Threshold = f2fx(v: std::max(a: 5.0f, b: 10.0f * MapView()->MouseWorldScale()));
1223 ivec2 SmallestDiff = ivec2(Threshold + 1, Threshold + 1);
1224 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1225
1226 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
1227
1228 auto &&CheckAlignment = [&](CPoint &Aligned, int Point) {
1229 CPoint ToCheck = AABB.m_aPoints[Point] + Offset;
1230 ivec2 DirectedDiff = Aligned - ToCheck;
1231 ivec2 Diff = ivec2(absolute(a: DirectedDiff.x), absolute(a: DirectedDiff.y));
1232
1233 if(Diff.x <= Threshold && (!GridEnabled || Diff.x == 0))
1234 {
1235 if(Diff.x < SmallestDiff.x)
1236 {
1237 SmallestDiff.x = Diff.x;
1238 vAlignmentsX.clear();
1239 }
1240
1241 if(Diff.x == SmallestDiff.x)
1242 {
1243 vAlignmentsX.push_back(x: SAlignmentInfo{
1244 .m_AlignedPoint: Aligned,
1245 {.m_X: AABB.m_aPoints[Point].y},
1246 .m_Axis: EAxis::Y,
1247 .m_PointIndex: Point,
1248 .m_Diff: DirectedDiff.x,
1249 });
1250 }
1251 }
1252
1253 if(Diff.y <= Threshold && (!GridEnabled || Diff.y == 0))
1254 {
1255 if(Diff.y < SmallestDiff.y)
1256 {
1257 SmallestDiff.y = Diff.y;
1258 vAlignmentsY.clear();
1259 }
1260
1261 if(Diff.y == SmallestDiff.y)
1262 {
1263 vAlignmentsY.push_back(x: SAlignmentInfo{
1264 .m_AlignedPoint: Aligned,
1265 {.m_X: AABB.m_aPoints[Point].x},
1266 .m_Axis: EAxis::X,
1267 .m_PointIndex: Point,
1268 .m_Diff: DirectedDiff.y,
1269 });
1270 }
1271 }
1272 };
1273
1274 auto &&CheckAABBAlignment = [&](CPoint &QuadMin, CPoint &QuadMax) {
1275 CPoint QuadCenter = (QuadMin + QuadMax) / 2.0f;
1276 CPoint aQuadPoints[5] = {
1277 QuadMin, // Top left
1278 {QuadMax.x, QuadMin.y}, // Top right
1279 {QuadMin.x, QuadMax.y}, // Bottom left
1280 QuadMax, // Bottom right
1281 QuadCenter,
1282 };
1283
1284 // Check all points with all the other points
1285 for(auto &QuadPoint : aQuadPoints)
1286 {
1287 // i is the quad point which is "aligned" and that we want to compare with
1288 for(int j = 0; j < 5; j++)
1289 {
1290 // j is the point we try to align
1291 CheckAlignment(QuadPoint, j);
1292 }
1293 }
1294 };
1295
1296 // Iterate through all quads of the current layer
1297 // Compute AABB of all quads and check if the dragged AABB can be aligned to this AABB.
1298 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
1299 {
1300 auto *pCurrentQuad = &pLayer->m_vQuads[i];
1301 if(Map()->IsQuadSelected(Index: i)) // Don't check with other selected quads
1302 continue;
1303
1304 // Get AABB of this quad
1305 CPoint QuadMin = pCurrentQuad->m_aPoints[0], QuadMax = pCurrentQuad->m_aPoints[0];
1306 for(int v = 1; v < 4; v++)
1307 {
1308 QuadMin.x = std::min(a: QuadMin.x, b: pCurrentQuad->m_aPoints[v].x);
1309 QuadMin.y = std::min(a: QuadMin.y, b: pCurrentQuad->m_aPoints[v].y);
1310 QuadMax.x = std::max(a: QuadMax.x, b: pCurrentQuad->m_aPoints[v].x);
1311 QuadMax.y = std::max(a: QuadMax.y, b: pCurrentQuad->m_aPoints[v].y);
1312 }
1313
1314 CheckAABBAlignment(QuadMin, QuadMax);
1315 }
1316
1317 // Finally, concatenate both alignment vectors into the output
1318 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1319 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1320 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1321}
1322
1323void CEditor::DrawPointAlignments(const std::vector<SAlignmentInfo> &vAlignments, ivec2 Offset) const
1324{
1325 if(!g_Config.m_EdAlignQuads)
1326 return;
1327
1328 // Drawing an alignment is easy, we convert fixed to float for the aligned point coords
1329 // and we also convert the "changing" value after applying the offset (which might be edited to actually align the value with the alignment).
1330 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1331 for(const SAlignmentInfo &Alignment : vAlignments)
1332 {
1333 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1334 if(Alignment.m_Axis == EAxis::X)
1335 { // Alignment on X axis is same Y values but different X values
1336 IGraphics::CQuadItem Line(fx2f(v: Alignment.m_AlignedPoint.x), fx2f(v: Alignment.m_AlignedPoint.y), fx2f(v: Alignment.m_X + Offset.x - Alignment.m_AlignedPoint.x), 1.0f * MapView()->MouseWorldScale());
1337 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1338 }
1339 else if(Alignment.m_Axis == EAxis::Y)
1340 { // Alignment on Y axis is same X values but different Y values
1341 IGraphics::CQuadItem Line(fx2f(v: Alignment.m_AlignedPoint.x), fx2f(v: Alignment.m_AlignedPoint.y), 1.0f * MapView()->MouseWorldScale(), fx2f(v: Alignment.m_Y + Offset.y - Alignment.m_AlignedPoint.y));
1342 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1343 }
1344 }
1345}
1346
1347void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, ivec2 Offset) const
1348{
1349 // Drawing an AABB is simply converting the points from fixed to float
1350 // Then making lines out of quads and drawing them
1351 vec2 TL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].y + Offset.y)};
1352 vec2 TR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].y + Offset.y)};
1353 vec2 BL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].y + Offset.y)};
1354 vec2 BR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].y + Offset.y)};
1355 vec2 Center = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].x + Offset.x), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].y + Offset.y)};
1356
1357 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1358 IGraphics::CQuadItem Lines[4] = {
1359 {TL.x, TL.y, TR.x - TL.x, 1.0f * MapView()->MouseWorldScale()},
1360 {TL.x, TL.y, 1.0f * MapView()->MouseWorldScale(), BL.y - TL.y},
1361 {TR.x, TR.y, 1.0f * MapView()->MouseWorldScale(), BR.y - TR.y},
1362 {BL.x, BL.y, BR.x - BL.x, 1.0f * MapView()->MouseWorldScale()},
1363 };
1364 Graphics()->SetColor(r: 1, g: 0, b: 1, a: 1);
1365 Graphics()->QuadsDrawTL(pArray: Lines, Num: 4);
1366
1367 IGraphics::CQuadItem CenterQuad(Center.x, Center.y, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
1368 Graphics()->QuadsDraw(pArray: &CenterQuad, Num: 1);
1369}
1370
1371void CEditor::QuadSelectionAABB(const std::shared_ptr<CLayerQuads> &pLayer, SAxisAlignedBoundingBox &OutAABB)
1372{
1373 // Compute an englobing AABB of the current selection of quads
1374 CPoint Min{
1375 std::numeric_limits<int>::max(),
1376 std::numeric_limits<int>::max(),
1377 };
1378 CPoint Max{
1379 std::numeric_limits<int>::min(),
1380 std::numeric_limits<int>::min(),
1381 };
1382 for(int Selected : Map()->m_vSelectedQuads)
1383 {
1384 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1385 for(int i = 0; i < 4; i++)
1386 {
1387 auto *pPoint = &pQuad->m_aPoints[i];
1388 Min.x = std::min(a: Min.x, b: pPoint->x);
1389 Min.y = std::min(a: Min.y, b: pPoint->y);
1390 Max.x = std::max(a: Max.x, b: pPoint->x);
1391 Max.y = std::max(a: Max.y, b: pPoint->y);
1392 }
1393 }
1394 CPoint Center = (Min + Max) / 2.0f;
1395 CPoint aPoints[SAxisAlignedBoundingBox::NUM_POINTS] = {
1396 Min, // Top left
1397 {Max.x, Min.y}, // Top right
1398 {Min.x, Max.y}, // Bottom left
1399 Max, // Bottom right
1400 Center,
1401 };
1402 mem_copy(dest: OutAABB.m_aPoints, source: aPoints, size: sizeof(CPoint) * SAxisAlignedBoundingBox::NUM_POINTS);
1403}
1404
1405void CEditor::ApplyAlignments(const std::vector<SAlignmentInfo> &vAlignments, ivec2 &Offset)
1406{
1407 if(vAlignments.empty())
1408 return;
1409
1410 // To find the alignments we simply iterate through the vector of alignments and find the first
1411 // X and Y alignments.
1412 // Then, we use the saved m_Diff to adjust the offset
1413 bvec2 GotAdjust = bvec2(false, false);
1414 ivec2 Adjust = ivec2(0, 0);
1415 for(const SAlignmentInfo &Alignment : vAlignments)
1416 {
1417 if(Alignment.m_Axis == EAxis::X && !GotAdjust.y)
1418 {
1419 GotAdjust.y = true;
1420 Adjust.y = Alignment.m_Diff;
1421 }
1422 else if(Alignment.m_Axis == EAxis::Y && !GotAdjust.x)
1423 {
1424 GotAdjust.x = true;
1425 Adjust.x = Alignment.m_Diff;
1426 }
1427 }
1428
1429 Offset += Adjust;
1430}
1431
1432void CEditor::ApplyAxisAlignment(ivec2 &Offset) const
1433{
1434 // This is used to preserve axis alignment when pressing `Shift`
1435 // Should be called before any other computation
1436 EAxis Axis = GetDragAxis(Offset);
1437 Offset.x = ((Axis == EAxis::NONE || Axis == EAxis::X) ? Offset.x : 0);
1438 Offset.y = ((Axis == EAxis::NONE || Axis == EAxis::Y) ? Offset.y : 0);
1439}
1440
1441static CColor AverageColor(const std::vector<CQuad *> &vpQuads)
1442{
1443 CColor Average = {0, 0, 0, 0};
1444 for(CQuad *pQuad : vpQuads)
1445 {
1446 for(CColor Color : pQuad->m_aColors)
1447 {
1448 Average += Color;
1449 }
1450 }
1451 return Average / std::size(CQuad{}.m_aColors) / vpQuads.size();
1452}
1453
1454void CEditor::DoQuad(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int Index)
1455{
1456 enum
1457 {
1458 OP_NONE = 0,
1459 OP_SELECT,
1460 OP_MOVE_ALL,
1461 OP_MOVE_PIVOT,
1462 OP_ROTATE,
1463 OP_CONTEXT_MENU,
1464 OP_DELETE,
1465 };
1466
1467 // some basic values
1468 const void *pId = &pQuad->m_aPoints[4]; // use pivot addr as id
1469 static std::vector<std::vector<CPoint>> s_vvRotatePoints;
1470 static int s_Operation = OP_NONE;
1471 static vec2 s_MouseStart = vec2(0.0f, 0.0f);
1472 static float s_RotateAngle = 0;
1473 static CPoint s_OriginalPosition;
1474 static std::vector<SAlignmentInfo> s_PivotAlignments; // Alignments per pivot per quad
1475 static std::vector<SAlignmentInfo> s_vAABBAlignments; // Alignments for one AABB (single quad or selection of multiple quads)
1476 static SAxisAlignedBoundingBox s_SelectionAABB; // Selection AABB
1477 static ivec2 s_LastOffset; // Last offset, stored as static so we can use it to draw every frame
1478
1479 // get pivot
1480 float CenterX = fx2f(v: pQuad->m_aPoints[4].x);
1481 float CenterY = fx2f(v: pQuad->m_aPoints[4].y);
1482
1483 const bool IgnoreGrid = Input()->AltIsPressed();
1484
1485 auto &&GetDragOffset = [&]() -> ivec2 {
1486 vec2 Pos = MapView()->MouseWorldPos();
1487 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
1488 {
1489 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
1490 }
1491 return ivec2(f2fx(v: Pos.x) - s_OriginalPosition.x, f2fx(v: Pos.y) - s_OriginalPosition.y);
1492 };
1493
1494 // draw selection background
1495 if(Map()->IsQuadSelected(Index))
1496 {
1497 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
1498 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 7.0f * MapView()->MouseWorldScale(), 7.0f * MapView()->MouseWorldScale());
1499 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1500 }
1501
1502 if(Ui()->CheckActiveItem(pId))
1503 {
1504 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
1505 {
1506 if(s_Operation == OP_SELECT)
1507 {
1508 if(distance_squared(a: s_MouseStart, b: Ui()->MousePos()) > 20.0f)
1509 {
1510 if(!Map()->IsQuadSelected(Index))
1511 Map()->SelectQuad(Index);
1512
1513 s_OriginalPosition = pQuad->m_aPoints[4];
1514
1515 if(Input()->ShiftIsPressed())
1516 {
1517 s_Operation = OP_MOVE_PIVOT;
1518 // When moving, we need to save the original position of all selected pivots
1519 for(int Selected : Map()->m_vSelectedQuads)
1520 {
1521 const CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1522 PreparePointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4);
1523 }
1524 }
1525 else
1526 {
1527 s_Operation = OP_MOVE_ALL;
1528 // When moving, we need to save the original position of all selected quads points
1529 for(int Selected : Map()->m_vSelectedQuads)
1530 {
1531 const CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1532 for(size_t v = 0; v < 5; v++)
1533 PreparePointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v);
1534 }
1535 // And precompute AABB of selection since it will not change during drag
1536 if(g_Config.m_EdAlignQuads)
1537 QuadSelectionAABB(pLayer, OutAABB&: s_SelectionAABB);
1538 }
1539 }
1540 }
1541
1542 // check if we only should move pivot
1543 if(s_Operation == OP_MOVE_PIVOT)
1544 {
1545 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1546
1547 s_LastOffset = GetDragOffset(); // Update offset
1548 ApplyAxisAlignment(Offset&: s_LastOffset); // Apply axis alignment to the offset
1549
1550 ComputePointsAlignments(pLayer, Pivot: true, Offset: s_LastOffset, vAlignments&: s_PivotAlignments);
1551 ApplyAlignments(vAlignments: s_PivotAlignments, Offset&: s_LastOffset);
1552
1553 for(auto &Selected : Map()->m_vSelectedQuads)
1554 {
1555 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1556 DoPointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4, Offset: s_LastOffset);
1557 }
1558 }
1559 else if(s_Operation == OP_MOVE_ALL)
1560 {
1561 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1562
1563 // Compute drag offset
1564 s_LastOffset = GetDragOffset();
1565 ApplyAxisAlignment(Offset&: s_LastOffset);
1566
1567 // Then compute possible alignments with the selection AABB
1568 ComputeAABBAlignments(pLayer, AABB: s_SelectionAABB, Offset: s_LastOffset, vAlignments&: s_vAABBAlignments);
1569 // Apply alignments before drag
1570 ApplyAlignments(vAlignments: s_vAABBAlignments, Offset&: s_LastOffset);
1571 // Then do the drag
1572 for(int Selected : Map()->m_vSelectedQuads)
1573 {
1574 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
1575 for(int v = 0; v < 5; v++)
1576 DoPointDrag(pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v, Offset: s_LastOffset);
1577 }
1578 }
1579 else if(s_Operation == OP_ROTATE)
1580 {
1581 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1582
1583 for(size_t i = 0; i < Map()->m_vSelectedQuads.size(); ++i)
1584 {
1585 CQuad *pCurrentQuad = &pLayer->m_vQuads[Map()->m_vSelectedQuads[i]];
1586 for(int v = 0; v < 4; v++)
1587 {
1588 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
1589 Rotate(pCenter: &pCurrentQuad->m_aPoints[4], pPoint: &pCurrentQuad->m_aPoints[v], Rotation: s_RotateAngle);
1590 }
1591 }
1592
1593 s_RotateAngle += Ui()->MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.0001f : 0.002f);
1594 }
1595 }
1596
1597 // Draw axis and alignments when moving
1598 if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
1599 {
1600 EAxis Axis = GetDragAxis(Offset: s_LastOffset);
1601 DrawAxis(Axis, OriginalPoint&: s_OriginalPosition, Point&: pQuad->m_aPoints[4]);
1602
1603 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
1604 }
1605
1606 if(s_Operation == OP_MOVE_PIVOT)
1607 DrawPointAlignments(vAlignments: s_PivotAlignments, Offset: s_LastOffset);
1608
1609 if(s_Operation == OP_MOVE_ALL)
1610 {
1611 DrawPointAlignments(vAlignments: s_vAABBAlignments, Offset: s_LastOffset);
1612
1613 if(g_Config.m_EdShowQuadsRect)
1614 DrawAABB(AABB: s_SelectionAABB, Offset: s_LastOffset);
1615 }
1616
1617 if(s_Operation == OP_CONTEXT_MENU)
1618 {
1619 if(!Ui()->MouseButton(Index: 1))
1620 {
1621 if(Map()->m_vSelectedLayers.size() == 1)
1622 {
1623 m_QuadPopupContext.m_pEditor = this;
1624 m_QuadPopupContext.m_SelectedQuadIndex = Map()->FindSelectedQuadIndex(Index);
1625 dbg_assert(m_QuadPopupContext.m_SelectedQuadIndex >= 0, "Selected quad index not found for quad popup");
1626 m_QuadPopupContext.m_Color = PackColor(Color: AverageColor(vpQuads: Map()->SelectedQuads()));
1627 Ui()->DoPopupMenu(pId: &m_QuadPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 251, pContext: &m_QuadPopupContext, pfnFunc: PopupQuad);
1628 Ui()->DisableMouseLock();
1629 }
1630 s_Operation = OP_NONE;
1631 Ui()->SetActiveItem(nullptr);
1632 }
1633 }
1634 else if(s_Operation == OP_DELETE)
1635 {
1636 if(!Ui()->MouseButton(Index: 1))
1637 {
1638 if(Map()->m_vSelectedLayers.size() == 1)
1639 {
1640 Ui()->DisableMouseLock();
1641 Map()->OnModify();
1642 Map()->DeleteSelectedQuads();
1643 }
1644 s_Operation = OP_NONE;
1645 Ui()->SetActiveItem(nullptr);
1646 }
1647 }
1648 else if(s_Operation == OP_ROTATE)
1649 {
1650 if(Ui()->MouseButton(Index: 0))
1651 {
1652 Ui()->DisableMouseLock();
1653 s_Operation = OP_NONE;
1654 Ui()->SetActiveItem(nullptr);
1655 Map()->m_QuadTracker.EndQuadTrack();
1656 }
1657 else if(Ui()->MouseButton(Index: 1))
1658 {
1659 Ui()->DisableMouseLock();
1660 s_Operation = OP_NONE;
1661 Ui()->SetActiveItem(nullptr);
1662
1663 // Reset points to old position
1664 for(size_t i = 0; i < Map()->m_vSelectedQuads.size(); ++i)
1665 {
1666 CQuad *pCurrentQuad = &pLayer->m_vQuads[Map()->m_vSelectedQuads[i]];
1667 for(int v = 0; v < 4; v++)
1668 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
1669 }
1670 }
1671 }
1672 else
1673 {
1674 if(!Ui()->MouseButton(Index: 0))
1675 {
1676 if(s_Operation == OP_SELECT)
1677 {
1678 if(Input()->ShiftIsPressed())
1679 Map()->ToggleSelectQuad(Index);
1680 else
1681 Map()->SelectQuad(Index);
1682 }
1683 else if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
1684 {
1685 Map()->m_QuadTracker.EndQuadTrack();
1686 }
1687
1688 Ui()->DisableMouseLock();
1689 s_Operation = OP_NONE;
1690 Ui()->SetActiveItem(nullptr);
1691
1692 s_LastOffset = ivec2();
1693 s_OriginalPosition = ivec2();
1694 s_vAABBAlignments.clear();
1695 s_PivotAlignments.clear();
1696 }
1697 }
1698
1699 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1700 }
1701 else if(Input()->KeyPress(Key: KEY_R) && !Map()->m_vSelectedQuads.empty() && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Ui()->IsPopupOpen())
1702 {
1703 Ui()->EnableMouseLock(pId);
1704 Ui()->SetActiveItem(pId);
1705 s_Operation = OP_ROTATE;
1706 s_RotateAngle = 0;
1707
1708 s_vvRotatePoints.clear();
1709 s_vvRotatePoints.resize(sz: Map()->m_vSelectedQuads.size());
1710 for(size_t i = 0; i < Map()->m_vSelectedQuads.size(); ++i)
1711 {
1712 CQuad *pCurrentQuad = &pLayer->m_vQuads[Map()->m_vSelectedQuads[i]];
1713
1714 s_vvRotatePoints[i].resize(sz: 4);
1715 s_vvRotatePoints[i][0] = pCurrentQuad->m_aPoints[0];
1716 s_vvRotatePoints[i][1] = pCurrentQuad->m_aPoints[1];
1717 s_vvRotatePoints[i][2] = pCurrentQuad->m_aPoints[2];
1718 s_vvRotatePoints[i][3] = pCurrentQuad->m_aPoints[3];
1719 }
1720 }
1721 else if(Ui()->HotItem() == pId)
1722 {
1723 m_pUiGotContext = pId;
1724
1725 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1726 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold shift to move pivot. Hold alt to ignore grid. Shift+right click to delete.");
1727
1728 if(Ui()->MouseButton(Index: 0))
1729 {
1730 Ui()->SetActiveItem(pId);
1731
1732 s_MouseStart = Ui()->MousePos();
1733 s_Operation = OP_SELECT;
1734 }
1735 else if(Ui()->MouseButtonClicked(Index: 1))
1736 {
1737 if(Input()->ShiftIsPressed())
1738 {
1739 s_Operation = OP_DELETE;
1740
1741 if(!Map()->IsQuadSelected(Index))
1742 Map()->SelectQuad(Index);
1743
1744 Ui()->SetActiveItem(pId);
1745 }
1746 else
1747 {
1748 s_Operation = OP_CONTEXT_MENU;
1749
1750 if(!Map()->IsQuadSelected(Index))
1751 Map()->SelectQuad(Index);
1752
1753 Ui()->SetActiveItem(pId);
1754 }
1755 }
1756 }
1757 else
1758 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
1759
1760 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
1761 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1762}
1763
1764void CEditor::DoQuadPoint(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int V)
1765{
1766 const void *pId = &pQuad->m_aPoints[V];
1767 const vec2 Center = vec2(fx2f(v: pQuad->m_aPoints[V].x), fx2f(v: pQuad->m_aPoints[V].y));
1768 const bool IgnoreGrid = Input()->AltIsPressed();
1769
1770 // draw selection background
1771 if(Map()->IsQuadPointSelected(QuadIndex, Index: V))
1772 {
1773 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
1774 IGraphics::CQuadItem QuadItem(Center.x, Center.y, 7.0f * MapView()->MouseWorldScale(), 7.0f * MapView()->MouseWorldScale());
1775 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1776 }
1777
1778 enum
1779 {
1780 OP_NONE = 0,
1781 OP_SELECT,
1782 OP_MOVEPOINT,
1783 OP_MOVEUV,
1784 OP_CONTEXT_MENU
1785 };
1786
1787 static int s_Operation = OP_NONE;
1788 static vec2 s_MouseStart = vec2(0.0f, 0.0f);
1789 static CPoint s_OriginalPoint;
1790 static std::vector<SAlignmentInfo> s_Alignments; // Alignments
1791 static ivec2 s_LastOffset;
1792
1793 auto &&GetDragOffset = [&]() -> ivec2 {
1794 vec2 Pos = MapView()->MouseWorldPos();
1795 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
1796 {
1797 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
1798 }
1799 return ivec2(f2fx(v: Pos.x) - s_OriginalPoint.x, f2fx(v: Pos.y) - s_OriginalPoint.y);
1800 };
1801
1802 if(Ui()->CheckActiveItem(pId))
1803 {
1804 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
1805 {
1806 if(s_Operation == OP_SELECT)
1807 {
1808 if(distance_squared(a: s_MouseStart, b: Ui()->MousePos()) > 20.0f)
1809 {
1810 if(!Map()->IsQuadPointSelected(QuadIndex, Index: V))
1811 Map()->SelectQuadPoint(QuadIndex, Index: V);
1812
1813 if(Input()->ShiftIsPressed())
1814 {
1815 s_Operation = OP_MOVEUV;
1816 Ui()->EnableMouseLock(pId);
1817 }
1818 else
1819 {
1820 s_Operation = OP_MOVEPOINT;
1821 // Save original positions before moving
1822 s_OriginalPoint = pQuad->m_aPoints[V];
1823 for(int Selected : Map()->m_vSelectedQuads)
1824 {
1825 for(int m = 0; m < 4; m++)
1826 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1827 PreparePointDrag(pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m);
1828 }
1829 }
1830 }
1831 }
1832
1833 if(s_Operation == OP_MOVEPOINT)
1834 {
1835 Map()->m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, GroupIndex: -1, LayerIndex);
1836
1837 s_LastOffset = GetDragOffset(); // Update offset
1838 ApplyAxisAlignment(Offset&: s_LastOffset); // Apply axis alignment to offset
1839
1840 ComputePointsAlignments(pLayer, Pivot: false, Offset: s_LastOffset, vAlignments&: s_Alignments);
1841 ApplyAlignments(vAlignments: s_Alignments, Offset&: s_LastOffset);
1842
1843 for(int Selected : Map()->m_vSelectedQuads)
1844 {
1845 for(int m = 0; m < 4; m++)
1846 {
1847 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1848 {
1849 DoPointDrag(pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m, Offset: s_LastOffset);
1850 }
1851 }
1852 }
1853 }
1854 else if(s_Operation == OP_MOVEUV)
1855 {
1856 int SelectedPoints = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3);
1857
1858 Map()->m_QuadTracker.BeginQuadPointPropTrack(pLayer, vSelectedQuads: Map()->m_vSelectedQuads, SelectedQuadPoints: SelectedPoints, GroupIndex: -1, LayerIndex);
1859 Map()->m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::TEX_U);
1860 Map()->m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::TEX_V);
1861
1862 for(int Selected : Map()->m_vSelectedQuads)
1863 {
1864 CQuad *pSelectedQuad = &pLayer->m_vQuads[Selected];
1865 for(int m = 0; m < 4; m++)
1866 {
1867 if(Map()->IsQuadPointSelected(QuadIndex: Selected, Index: m))
1868 {
1869 // 0,2;1,3 - line x
1870 // 0,1;2,3 - line y
1871
1872 pSelectedQuad->m_aTexcoords[m].x += f2fx(v: MapView()->MouseDeltaWorld().x * 0.001f);
1873 pSelectedQuad->m_aTexcoords[(m + 2) % 4].x += f2fx(v: MapView()->MouseDeltaWorld().x * 0.001f);
1874
1875 pSelectedQuad->m_aTexcoords[m].y += f2fx(v: MapView()->MouseDeltaWorld().y * 0.001f);
1876 pSelectedQuad->m_aTexcoords[m ^ 1].y += f2fx(v: MapView()->MouseDeltaWorld().y * 0.001f);
1877 }
1878 }
1879 }
1880 }
1881 }
1882
1883 // Draw axis and alignments when dragging
1884 if(s_Operation == OP_MOVEPOINT)
1885 {
1886 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1887
1888 // Axis
1889 EAxis Axis = GetDragAxis(Offset: s_LastOffset);
1890 DrawAxis(Axis, OriginalPoint&: s_OriginalPoint, Point&: pQuad->m_aPoints[V]);
1891
1892 // Alignments
1893 DrawPointAlignments(vAlignments: s_Alignments, Offset: s_LastOffset);
1894
1895 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
1896 }
1897
1898 if(s_Operation == OP_CONTEXT_MENU)
1899 {
1900 if(!Ui()->MouseButton(Index: 1))
1901 {
1902 if(Map()->m_vSelectedLayers.size() == 1)
1903 {
1904 if(!Map()->IsQuadSelected(Index: QuadIndex))
1905 Map()->SelectQuad(Index: QuadIndex);
1906
1907 m_PointPopupContext.m_pEditor = this;
1908 m_PointPopupContext.m_SelectedQuadPoint = V;
1909 m_PointPopupContext.m_SelectedQuadIndex = Map()->FindSelectedQuadIndex(Index: QuadIndex);
1910 dbg_assert(m_PointPopupContext.m_SelectedQuadIndex >= 0, "Selected quad index not found for quad point popup");
1911 Ui()->DoPopupMenu(pId: &m_PointPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 75, pContext: &m_PointPopupContext, pfnFunc: PopupPoint);
1912 }
1913 Ui()->SetActiveItem(nullptr);
1914 }
1915 }
1916 else
1917 {
1918 if(!Ui()->MouseButton(Index: 0))
1919 {
1920 if(s_Operation == OP_SELECT)
1921 {
1922 if(Input()->ShiftIsPressed())
1923 Map()->ToggleSelectQuadPoint(QuadIndex, Index: V);
1924 else
1925 Map()->SelectQuadPoint(QuadIndex, Index: V);
1926 }
1927
1928 if(s_Operation == OP_MOVEPOINT)
1929 {
1930 Map()->m_QuadTracker.EndQuadTrack();
1931 }
1932 else if(s_Operation == OP_MOVEUV)
1933 {
1934 Map()->m_QuadTracker.EndQuadPointPropTrackAll();
1935 }
1936
1937 Ui()->DisableMouseLock();
1938 Ui()->SetActiveItem(nullptr);
1939 }
1940 }
1941
1942 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1943 }
1944 else if(Ui()->HotItem() == pId)
1945 {
1946 m_pUiGotContext = pId;
1947
1948 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1949 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold shift to move the texture. Hold alt to ignore grid.");
1950
1951 if(Ui()->MouseButton(Index: 0))
1952 {
1953 Ui()->SetActiveItem(pId);
1954
1955 s_MouseStart = Ui()->MousePos();
1956 s_Operation = OP_SELECT;
1957 }
1958 else if(Ui()->MouseButtonClicked(Index: 1))
1959 {
1960 s_Operation = OP_CONTEXT_MENU;
1961
1962 Ui()->SetActiveItem(pId);
1963
1964 if(!Map()->IsQuadPointSelected(QuadIndex, Index: V))
1965 Map()->SelectQuadPoint(QuadIndex, Index: V);
1966 }
1967 }
1968 else
1969 Graphics()->SetColor(r: 1, g: 0, b: 0, a: 1);
1970
1971 IGraphics::CQuadItem QuadItem(Center.x, Center.y, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
1972 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1973}
1974
1975void CEditor::DoQuadEnvelopes(const CLayerQuads *pLayerQuads)
1976{
1977 const std::vector<CQuad> &vQuads = pLayerQuads->m_vQuads;
1978 if(vQuads.empty())
1979 {
1980 return;
1981 }
1982
1983 std::vector<std::pair<const CQuad *, CEnvelope *>> vQuadsWithEnvelopes;
1984 vQuadsWithEnvelopes.reserve(n: vQuads.size());
1985 for(const auto &Quad : vQuads)
1986 {
1987 if(m_ActiveEnvelopePreview != EEnvelopePreview::ALL &&
1988 !(m_ActiveEnvelopePreview == EEnvelopePreview::SELECTED && Quad.m_PosEnv == Map()->m_SelectedEnvelope))
1989 {
1990 continue;
1991 }
1992 if(Quad.m_PosEnv < 0 ||
1993 Quad.m_PosEnv >= (int)Map()->m_vpEnvelopes.size() ||
1994 Map()->m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints.empty())
1995 {
1996 continue;
1997 }
1998 vQuadsWithEnvelopes.emplace_back(args: &Quad, args: Map()->m_vpEnvelopes[Quad.m_PosEnv].get());
1999 }
2000 if(vQuadsWithEnvelopes.empty())
2001 {
2002 return;
2003 }
2004
2005 Map()->SelectedGroup()->MapScreen();
2006
2007 // Draw lines between points
2008 Graphics()->TextureClear();
2009 IGraphics::CLineItemBatch LineItemBatch;
2010 Graphics()->LinesBatchBegin(pBatch: &LineItemBatch);
2011 Graphics()->SetColor(ColorRGBA(0.0f, 1.0f, 1.0f, 0.75f));
2012 for(const auto &[pQuad, pEnvelope] : vQuadsWithEnvelopes)
2013 {
2014 if(pEnvelope->m_vPoints.size() < 2)
2015 {
2016 continue;
2017 }
2018
2019 const CPoint *pPivotPoint = &pQuad->m_aPoints[4];
2020 const vec2 PivotPoint = vec2(fx2f(v: pPivotPoint->x), fx2f(v: pPivotPoint->y));
2021
2022 for(int PointIndex = 0; PointIndex <= (int)pEnvelope->m_vPoints.size() - 2; PointIndex++)
2023 {
2024 const auto &PointStart = pEnvelope->m_vPoints[PointIndex];
2025 const auto &PointEnd = pEnvelope->m_vPoints[PointIndex + 1];
2026 const float PointStartTime = PointStart.m_Time.AsSeconds();
2027 const float PointEndTime = PointEnd.m_Time.AsSeconds();
2028 const float TimeRange = PointEndTime - PointStartTime;
2029
2030 int Steps;
2031 if(PointStart.m_Curvetype == CURVETYPE_BEZIER)
2032 {
2033 Steps = std::clamp(val: round_to_int(f: TimeRange * 10.0f), lo: 50, hi: 150);
2034 }
2035 else
2036 {
2037 Steps = 1;
2038 }
2039 ColorRGBA StartPosition = PointStart.ColorValue();
2040 for(int Step = 1; Step <= Steps; Step++)
2041 {
2042 ColorRGBA EndPosition;
2043 if(Step == Steps)
2044 {
2045 EndPosition = PointEnd.ColorValue();
2046 }
2047 else
2048 {
2049 const float SectionEndTime = PointStartTime + TimeRange * (Step / (float)Steps);
2050 EndPosition = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
2051 pEnvelope->Eval(Time: SectionEndTime, Result&: EndPosition, Channels: 2);
2052 }
2053
2054 const vec2 Pos0 = PivotPoint + vec2(StartPosition.r, StartPosition.g);
2055 const vec2 Pos1 = PivotPoint + vec2(EndPosition.r, EndPosition.g);
2056 const IGraphics::CLineItem Item = IGraphics::CLineItem(Pos0, Pos1);
2057 Graphics()->LinesBatchDraw(pBatch: &LineItemBatch, pArray: &Item, Num: 1);
2058
2059 StartPosition = EndPosition;
2060 }
2061 }
2062 }
2063 Graphics()->LinesBatchEnd(pBatch: &LineItemBatch);
2064
2065 // Draw quads at points
2066 if(pLayerQuads->m_Image >= 0 && pLayerQuads->m_Image < (int)Map()->m_vpImages.size())
2067 {
2068 Graphics()->TextureSet(Texture: Map()->m_vpImages[pLayerQuads->m_Image]->m_Texture);
2069 }
2070 else
2071 {
2072 Graphics()->TextureClear();
2073 }
2074 Graphics()->QuadsBegin();
2075 for(const auto &[pQuad, pEnvelope] : vQuadsWithEnvelopes)
2076 {
2077 for(size_t PointIndex = 0; PointIndex < pEnvelope->m_vPoints.size(); PointIndex++)
2078 {
2079 const CEnvPoint_runtime &EnvPoint = pEnvelope->m_vPoints[PointIndex];
2080 const vec2 Offset = vec2(fx2f(v: EnvPoint.m_aValues[0]), fx2f(v: EnvPoint.m_aValues[1]));
2081 const float Rotation = fx2f(v: EnvPoint.m_aValues[2]) / 180.0f * pi;
2082
2083 const float Alpha = (Map()->m_SelectedQuadEnvelope == pQuad->m_PosEnv && Map()->IsEnvPointSelected(Index: PointIndex)) ? 0.65f : 0.35f;
2084 Graphics()->SetColor4(
2085 TopLeft: ColorRGBA(pQuad->m_aColors[0].r, pQuad->m_aColors[0].g, pQuad->m_aColors[0].b, pQuad->m_aColors[0].a).Multiply(Factor: 1.0f / 255.0f).WithMultipliedAlpha(alpha: Alpha),
2086 TopRight: ColorRGBA(pQuad->m_aColors[1].r, pQuad->m_aColors[1].g, pQuad->m_aColors[1].b, pQuad->m_aColors[1].a).Multiply(Factor: 1.0f / 255.0f).WithMultipliedAlpha(alpha: Alpha),
2087 BottomLeft: ColorRGBA(pQuad->m_aColors[3].r, pQuad->m_aColors[3].g, pQuad->m_aColors[3].b, pQuad->m_aColors[3].a).Multiply(Factor: 1.0f / 255.0f).WithMultipliedAlpha(alpha: Alpha),
2088 BottomRight: ColorRGBA(pQuad->m_aColors[2].r, pQuad->m_aColors[2].g, pQuad->m_aColors[2].b, pQuad->m_aColors[2].a).Multiply(Factor: 1.0f / 255.0f).WithMultipliedAlpha(alpha: Alpha));
2089
2090 const CPoint *pPoints;
2091 CPoint aRotated[4];
2092 if(Rotation != 0.0f)
2093 {
2094 std::copy_n(first: pQuad->m_aPoints, n: std::size(aRotated), result: aRotated);
2095 for(auto &Point : aRotated)
2096 {
2097 Rotate(pCenter: &pQuad->m_aPoints[4], pPoint: &Point, Rotation);
2098 }
2099 pPoints = aRotated;
2100 }
2101 else
2102 {
2103 pPoints = pQuad->m_aPoints;
2104 }
2105 Graphics()->QuadsSetSubsetFree(
2106 x0: fx2f(v: pQuad->m_aTexcoords[0].x), y0: fx2f(v: pQuad->m_aTexcoords[0].y),
2107 x1: fx2f(v: pQuad->m_aTexcoords[1].x), y1: fx2f(v: pQuad->m_aTexcoords[1].y),
2108 x2: fx2f(v: pQuad->m_aTexcoords[2].x), y2: fx2f(v: pQuad->m_aTexcoords[2].y),
2109 x3: fx2f(v: pQuad->m_aTexcoords[3].x), y3: fx2f(v: pQuad->m_aTexcoords[3].y));
2110
2111 const IGraphics::CFreeformItem Freeform(
2112 fx2f(v: pPoints[0].x) + Offset.x, fx2f(v: pPoints[0].y) + Offset.y,
2113 fx2f(v: pPoints[1].x) + Offset.x, fx2f(v: pPoints[1].y) + Offset.y,
2114 fx2f(v: pPoints[2].x) + Offset.x, fx2f(v: pPoints[2].y) + Offset.y,
2115 fx2f(v: pPoints[3].x) + Offset.x, fx2f(v: pPoints[3].y) + Offset.y);
2116 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2117 }
2118 }
2119 Graphics()->QuadsEnd();
2120
2121 // Draw quad envelope point handles
2122 Graphics()->TextureClear();
2123 Graphics()->QuadsBegin();
2124 for(const auto &[pQuad, pEnvelope] : vQuadsWithEnvelopes)
2125 {
2126 for(size_t PointIndex = 0; PointIndex < pEnvelope->m_vPoints.size(); PointIndex++)
2127 {
2128 DoQuadEnvPoint(pQuad, pEnvelope, QuadIndex: pQuad - vQuads.data(), PointIndex);
2129 }
2130 }
2131 Graphics()->QuadsEnd();
2132}
2133
2134void CEditor::DoQuadEnvPoint(const CQuad *pQuad, CEnvelope *pEnvelope, int QuadIndex, int PointIndex)
2135{
2136 CEnvPoint_runtime *pPoint = &pEnvelope->m_vPoints[PointIndex];
2137 const vec2 Center = vec2(fx2f(v: pQuad->m_aPoints[4].x) + fx2f(v: pPoint->m_aValues[0]), fx2f(v: pQuad->m_aPoints[4].y) + fx2f(v: pPoint->m_aValues[1]));
2138 const bool IgnoreGrid = Input()->AltIsPressed();
2139
2140 if(Ui()->CheckActiveItem(pId: pPoint) && Map()->m_CurrentQuadIndex == QuadIndex)
2141 {
2142 if(MapView()->MouseDeltaWorld() != vec2(0.0f, 0.0f))
2143 {
2144 if(m_QuadEnvelopePointOperation == EQuadEnvelopePointOperation::MOVE)
2145 {
2146 vec2 Pos = MapView()->MouseWorldPos();
2147 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2148 {
2149 MapView()->MapGrid()->SnapToGrid(Position&: Pos);
2150 }
2151 pPoint->m_aValues[0] = f2fx(v: Pos.x) - pQuad->m_aPoints[4].x;
2152 pPoint->m_aValues[1] = f2fx(v: Pos.y) - pQuad->m_aPoints[4].y;
2153 }
2154 else if(m_QuadEnvelopePointOperation == EQuadEnvelopePointOperation::ROTATE)
2155 {
2156 pPoint->m_aValues[2] += 10 * Ui()->MouseDeltaX();
2157 }
2158 }
2159
2160 if(!Ui()->MouseButton(Index: 0))
2161 {
2162 Ui()->DisableMouseLock();
2163 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::NONE;
2164 Ui()->SetActiveItem(nullptr);
2165 }
2166
2167 Graphics()->SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2168 }
2169 else if(Ui()->HotItem() == pPoint && Map()->m_CurrentQuadIndex == QuadIndex)
2170 {
2171 Graphics()->SetColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2172 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold ctrl to rotate. Hold alt to ignore grid.");
2173
2174 if(Ui()->MouseButton(Index: 0))
2175 {
2176 if(Input()->ModifierIsPressed())
2177 {
2178 Ui()->EnableMouseLock(pId: pPoint);
2179 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::ROTATE;
2180 }
2181 else
2182 {
2183 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::MOVE;
2184 }
2185 Map()->SelectQuad(Index: QuadIndex);
2186 Map()->SelectEnvPoint(Index: PointIndex);
2187 Map()->m_SelectedQuadEnvelope = pQuad->m_PosEnv;
2188 Ui()->SetActiveItem(pPoint);
2189 }
2190 else
2191 {
2192 Map()->DeselectEnvPoints();
2193 Map()->m_SelectedQuadEnvelope = -1;
2194 }
2195 }
2196 else
2197 {
2198 Graphics()->SetColor(ColorRGBA(0.0f, 1.0f, 1.0f, 1.0f));
2199 }
2200
2201 IGraphics::CQuadItem QuadItem(Center.x, Center.y, 5.0f * MapView()->MouseWorldScale(), 5.0f * MapView()->MouseWorldScale());
2202 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2203}
2204
2205void CEditor::UpdateHotQuadPoint(const CLayerQuads *pLayer)
2206{
2207 const vec2 MouseWorld = MapView()->MouseWorldPos();
2208
2209 float MinDist = 500.0f;
2210 const void *pMinPointId = nullptr;
2211
2212 const auto UpdateMinimum = [&](vec2 Position, const void *pId) {
2213 const float CurrDist = distance_squared(a: Position, b: MouseWorld) / (MapView()->MouseWorldScale() * MapView()->MouseWorldScale());
2214 if(CurrDist < MinDist)
2215 {
2216 MinDist = CurrDist;
2217 pMinPointId = pId;
2218 return true;
2219 }
2220 return false;
2221 };
2222
2223 for(const CQuad &Quad : pLayer->m_vQuads)
2224 {
2225 if(m_ShowEnvelopePreview &&
2226 m_ActiveEnvelopePreview != EEnvelopePreview::NONE &&
2227 Quad.m_PosEnv >= 0 &&
2228 Quad.m_PosEnv < (int)Map()->m_vpEnvelopes.size())
2229 {
2230 for(const auto &EnvPoint : Map()->m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints)
2231 {
2232 const vec2 Position = vec2(fx2f(v: Quad.m_aPoints[4].x) + fx2f(v: EnvPoint.m_aValues[0]), fx2f(v: Quad.m_aPoints[4].y) + fx2f(v: EnvPoint.m_aValues[1]));
2233 if(UpdateMinimum(Position, &EnvPoint) && Ui()->ActiveItem() == nullptr)
2234 {
2235 Map()->m_CurrentQuadIndex = &Quad - pLayer->m_vQuads.data();
2236 }
2237 }
2238 }
2239
2240 for(const auto &Point : Quad.m_aPoints)
2241 {
2242 UpdateMinimum(vec2(fx2f(v: Point.x), fx2f(v: Point.y)), &Point);
2243 }
2244 }
2245
2246 if(pMinPointId != nullptr)
2247 {
2248 Ui()->SetHotItem(pMinPointId);
2249 }
2250}
2251
2252void CEditor::DoColorPickerButton(const void *pId, const CUIRect *pRect, ColorRGBA Color, const std::function<void(ColorRGBA Color)> &SetColor)
2253{
2254 CUIRect ColorRect;
2255 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * Ui()->ButtonColorMul(pId)), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
2256 pRect->Margin(Cut: 1.0f, pOtherRect: &ColorRect);
2257 ColorRect.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
2258
2259 const int ButtonResult = DoButtonLogic(pId, Checked: 0, pRect, Flags: BUTTONFLAG_ALL, pToolTip: "Click to show the color picker. Shift+right click to copy color to clipboard. Shift+left click to paste color from clipboard.");
2260 if(Input()->ShiftIsPressed())
2261 {
2262 if(ButtonResult == 1)
2263 {
2264 std::string Clipboard = Input()->GetClipboardText();
2265 if(Clipboard[0] == '#' || Clipboard[0] == '$') // ignore leading # (web color format) and $ (console color format)
2266 Clipboard = Clipboard.substr(pos: 1);
2267 if(str_isallnum_hex(str: Clipboard.c_str()))
2268 {
2269 std::optional<ColorRGBA> ParsedColor = color_parse<ColorRGBA>(pStr: Clipboard.c_str());
2270 if(ParsedColor)
2271 {
2272 m_ColorPickerPopupContext.m_State = EEditState::ONE_GO;
2273 SetColor(ParsedColor.value());
2274 }
2275 }
2276 }
2277 else if(ButtonResult == 2)
2278 {
2279 char aClipboard[9];
2280 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", Color.PackAlphaLast());
2281 Input()->SetClipboardText(aClipboard);
2282 }
2283 }
2284 else if(ButtonResult > 0)
2285 {
2286 if(m_ColorPickerPopupContext.m_ColorMode == CUi::SColorPickerPopupContext::MODE_UNSET)
2287 m_ColorPickerPopupContext.m_ColorMode = CUi::SColorPickerPopupContext::MODE_RGBA;
2288 m_ColorPickerPopupContext.m_RgbaColor = Color;
2289 m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(rgb: Color);
2290 m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(hsl: m_ColorPickerPopupContext.m_HslaColor);
2291 m_ColorPickerPopupContext.m_Alpha = true;
2292 m_pColorPickerPopupActiveId = pId;
2293 Ui()->ShowPopupColorPicker(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &m_ColorPickerPopupContext);
2294 }
2295
2296 if(Ui()->IsPopupOpen(pId: &m_ColorPickerPopupContext))
2297 {
2298 if(m_pColorPickerPopupActiveId == pId)
2299 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
2300 }
2301 else
2302 {
2303 m_pColorPickerPopupActiveId = nullptr;
2304 if(m_ColorPickerPopupContext.m_State == EEditState::EDITING)
2305 {
2306 m_ColorPickerPopupContext.m_State = EEditState::END;
2307 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
2308 m_ColorPickerPopupContext.m_State = EEditState::NONE;
2309 }
2310 }
2311}
2312
2313bool CEditor::IsAllowPlaceUnusedTiles() const
2314{
2315 // explicit allow and implicit allow
2316 return m_AllowPlaceUnusedTiles != EUnusedEntities::NOT_ALLOWED;
2317}
2318
2319void CEditor::CRenderLayersState::Reset()
2320{
2321 m_Operation = ELayerOperation::NONE;
2322 m_PreviousOperation = ELayerOperation::NONE;
2323 m_pDraggedButton = nullptr;
2324 m_InitialMouseY = 0.0f;
2325 m_InitialCutHeight = 0.0f;
2326 m_ScrollToSelectionNext = false;
2327 m_InitialGroupIndex = 0;
2328 m_vInitialLayerIndices.clear();
2329 m_LayerPopupContext = {};
2330}
2331
2332void CEditor::RenderLayers(CUIRect LayersBox)
2333{
2334 CRenderLayersState &State = m_RenderLayersState;
2335
2336 const float RowHeight = 12.0f;
2337 char aBuf[64];
2338
2339 CUIRect UnscrolledLayersBox = LayersBox;
2340
2341 CScrollRegion &ScrollRegion = Map()->m_EditorUiElements.m_LayersScrollRegion;
2342 CScrollRegionParams ScrollParams;
2343 ScrollParams.m_ScrollbarThickness = 10.0f;
2344 ScrollParams.m_ScrollbarMargin = 3.0f;
2345 ScrollParams.m_ScrollUnit = RowHeight * 5.0f;
2346 ScrollRegion.Begin(pClipRect: &LayersBox, pParams: &ScrollParams);
2347
2348 constexpr float MinDragDistance = 5.0f;
2349 int GroupAfterDraggedLayer = -1;
2350 int LayerAfterDraggedLayer = -1;
2351 bool DraggedPositionFound = false;
2352 bool MoveLayers = false;
2353 bool MoveGroup = false;
2354 bool StartDragLayer = false;
2355 bool StartDragGroup = false;
2356 std::vector<int> vButtonsPerGroup;
2357
2358 auto SetOperation = [&](ELayerOperation Operation) {
2359 if(Operation != State.m_Operation)
2360 {
2361 State.m_PreviousOperation = State.m_Operation;
2362 State.m_Operation = Operation;
2363 if(Operation == ELayerOperation::NONE)
2364 {
2365 State.m_pDraggedButton = nullptr;
2366 }
2367 }
2368 };
2369
2370 vButtonsPerGroup.reserve(n: Map()->m_vpGroups.size());
2371 for(const std::shared_ptr<CLayerGroup> &pGroup : Map()->m_vpGroups)
2372 {
2373 vButtonsPerGroup.push_back(x: pGroup->m_vpLayers.size() + 1);
2374 }
2375
2376 if(State.m_pDraggedButton != nullptr && Ui()->ActiveItem() != State.m_pDraggedButton)
2377 {
2378 SetOperation(ELayerOperation::NONE);
2379 }
2380
2381 if(State.m_Operation == ELayerOperation::LAYER_DRAG || State.m_Operation == ELayerOperation::GROUP_DRAG)
2382 {
2383 float MinDraggableValue = UnscrolledLayersBox.y;
2384 float MaxDraggableValue = MinDraggableValue;
2385 for(int NumButtons : vButtonsPerGroup)
2386 {
2387 MaxDraggableValue += NumButtons * (RowHeight + 2.0f) + 5.0f;
2388 }
2389 MaxDraggableValue += LayersBox.y - UnscrolledLayersBox.y;
2390
2391 if(State.m_Operation == ELayerOperation::GROUP_DRAG)
2392 {
2393 MaxDraggableValue -= vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f;
2394 }
2395 else if(State.m_Operation == ELayerOperation::LAYER_DRAG)
2396 {
2397 MinDraggableValue += RowHeight + 2.0f;
2398 MaxDraggableValue -= Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f) + 5.0f;
2399 }
2400
2401 UnscrolledLayersBox.HSplitTop(Cut: State.m_InitialCutHeight, pTop: nullptr, pBottom: &UnscrolledLayersBox);
2402 UnscrolledLayersBox.y -= State.m_InitialMouseY - Ui()->MouseY();
2403
2404 UnscrolledLayersBox.y = std::clamp(val: UnscrolledLayersBox.y, lo: MinDraggableValue, hi: MaxDraggableValue);
2405
2406 UnscrolledLayersBox.w = LayersBox.w;
2407 }
2408
2409 const bool ScrollToSelection = LayerSelector()->SelectByTile() || State.m_ScrollToSelectionNext;
2410 State.m_ScrollToSelectionNext = false;
2411
2412 // render layers
2413 for(int g = 0; g < (int)Map()->m_vpGroups.size(); g++)
2414 {
2415 if(State.m_Operation == ELayerOperation::LAYER_DRAG && g > 0 && !DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
2416 {
2417 DraggedPositionFound = true;
2418 GroupAfterDraggedLayer = g;
2419
2420 LayerAfterDraggedLayer = Map()->m_vpGroups[g - 1]->m_vpLayers.size();
2421
2422 CUIRect Slot;
2423 LayersBox.HSplitTop(Cut: Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &Slot, pBottom: &LayersBox);
2424 ScrollRegion.AddRect(Rect: Slot);
2425 }
2426
2427 CUIRect Slot, VisibleToggle;
2428 if(State.m_Operation == ELayerOperation::GROUP_DRAG)
2429 {
2430 if(g == Map()->m_SelectedGroup)
2431 {
2432 UnscrolledLayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &UnscrolledLayersBox);
2433 UnscrolledLayersBox.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &UnscrolledLayersBox);
2434 }
2435 else if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight * vButtonsPerGroup[g] / 2 + 3.0f)
2436 {
2437 DraggedPositionFound = true;
2438 GroupAfterDraggedLayer = g;
2439
2440 CUIRect TmpSlot;
2441 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_Collapse)
2442 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2443 else
2444 LayersBox.HSplitTop(Cut: vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2445 ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
2446 }
2447 }
2448 if(State.m_Operation != ELayerOperation::GROUP_DRAG || g != Map()->m_SelectedGroup)
2449 {
2450 LayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &LayersBox);
2451
2452 CUIRect TmpRect;
2453 LayersBox.HSplitTop(Cut: 2.0f, pTop: &TmpRect, pBottom: &LayersBox);
2454 ScrollRegion.AddRect(Rect: TmpRect);
2455 }
2456
2457 if(ScrollRegion.AddRect(Rect: Slot))
2458 {
2459 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Slot);
2460
2461 const int MouseClick = DoButton_FontIcon(pId: &Map()->m_vpGroups[g]->m_Visible, pText: Map()->m_vpGroups[g]->m_Visible ? FontIcon::EYE : FontIcon::EYE_SLASH, Checked: Map()->m_vpGroups[g]->m_Collapse ? 1 : 0, pRect: &VisibleToggle, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Left click to toggle visibility. Right click to show this group only.", Corners: IGraphics::CORNER_L, FontSize: 8.0f);
2462 if(MouseClick == 1)
2463 {
2464 Map()->m_vpGroups[g]->m_Visible = !Map()->m_vpGroups[g]->m_Visible;
2465 }
2466 else if(MouseClick == 2)
2467 {
2468 if(Input()->ShiftIsPressed())
2469 {
2470 if(g != Map()->m_SelectedGroup)
2471 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
2472 }
2473
2474 int NumActive = 0;
2475 for(auto &Group : Map()->m_vpGroups)
2476 {
2477 if(Group == Map()->m_vpGroups[g])
2478 {
2479 Group->m_Visible = true;
2480 continue;
2481 }
2482
2483 if(Group->m_Visible)
2484 {
2485 Group->m_Visible = false;
2486 NumActive++;
2487 }
2488 }
2489 if(NumActive == 0)
2490 {
2491 for(auto &Group : Map()->m_vpGroups)
2492 {
2493 Group->m_Visible = true;
2494 }
2495 }
2496 }
2497
2498 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "#%d %s", g, Map()->m_vpGroups[g]->m_aName);
2499
2500 bool Clicked;
2501 bool Abrupted;
2502 if(int Result = DoButton_DraggableEx(pId: Map()->m_vpGroups[g].get(), pText: aBuf, Checked: g == Map()->m_SelectedGroup, pRect: &Slot, pClicked: &Clicked, pAbrupted: &Abrupted,
2503 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: Map()->m_vpGroups[g]->m_Collapse ? "Select group. Shift+left click to select all layers. Double click to expand." : "Select group. Shift+left click to select all layers. Double click to collapse.", Corners: IGraphics::CORNER_R))
2504 {
2505 if(State.m_Operation == ELayerOperation::NONE)
2506 {
2507 State.m_InitialMouseY = Ui()->MouseY();
2508 State.m_InitialCutHeight = State.m_InitialMouseY - UnscrolledLayersBox.y;
2509 SetOperation(ELayerOperation::CLICK);
2510
2511 if(g != Map()->m_SelectedGroup)
2512 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
2513 }
2514
2515 if(Abrupted)
2516 {
2517 SetOperation(ELayerOperation::NONE);
2518 }
2519
2520 if(State.m_Operation == ELayerOperation::CLICK && absolute(a: Ui()->MouseY() - State.m_InitialMouseY) > MinDragDistance)
2521 {
2522 StartDragGroup = true;
2523 State.m_pDraggedButton = Map()->m_vpGroups[g].get();
2524 }
2525
2526 if(State.m_Operation == ELayerOperation::CLICK && Clicked)
2527 {
2528 if(g != Map()->m_SelectedGroup)
2529 Map()->SelectLayer(LayerIndex: 0, GroupIndex: g);
2530
2531 if(Input()->ShiftIsPressed() && Map()->m_SelectedGroup == g)
2532 {
2533 Map()->m_vSelectedLayers.clear();
2534 for(size_t i = 0; i < Map()->m_vpGroups[g]->m_vpLayers.size(); i++)
2535 {
2536 Map()->AddSelectedLayer(LayerIndex: i);
2537 }
2538 }
2539
2540 if(Result == 2)
2541 {
2542 Ui()->DoPopupMenu(pId: &State.m_PopupGroupId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 145, Height: 256, pContext: this, pfnFunc: PopupGroup);
2543 }
2544
2545 if(!Map()->m_vpGroups[g]->m_vpLayers.empty() && Ui()->DoDoubleClickLogic(pId: Map()->m_vpGroups[g].get()))
2546 Map()->m_vpGroups[g]->m_Collapse ^= 1;
2547
2548 SetOperation(ELayerOperation::NONE);
2549 }
2550
2551 if(State.m_Operation == ELayerOperation::GROUP_DRAG && Clicked)
2552 MoveGroup = true;
2553 }
2554 else if(State.m_pDraggedButton == Map()->m_vpGroups[g].get())
2555 {
2556 SetOperation(ELayerOperation::NONE);
2557 }
2558 }
2559
2560 for(int i = 0; i < (int)Map()->m_vpGroups[g]->m_vpLayers.size(); i++)
2561 {
2562 if(Map()->m_vpGroups[g]->m_Collapse)
2563 continue;
2564
2565 bool IsLayerSelected = false;
2566 if(Map()->m_SelectedGroup == g)
2567 {
2568 for(const auto &Selected : Map()->m_vSelectedLayers)
2569 {
2570 if(Selected == i)
2571 {
2572 IsLayerSelected = true;
2573 break;
2574 }
2575 }
2576 }
2577
2578 if(State.m_Operation == ELayerOperation::GROUP_DRAG && g == Map()->m_SelectedGroup)
2579 {
2580 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
2581 }
2582 else if(State.m_Operation == ELayerOperation::LAYER_DRAG)
2583 {
2584 if(IsLayerSelected)
2585 {
2586 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
2587 }
2588 else
2589 {
2590 if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
2591 {
2592 DraggedPositionFound = true;
2593 GroupAfterDraggedLayer = g + 1;
2594 LayerAfterDraggedLayer = i;
2595 for(size_t j = 0; j < Map()->m_vSelectedLayers.size(); j++)
2596 {
2597 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: nullptr, pBottom: &LayersBox);
2598 ScrollRegion.AddRect(Rect: Slot);
2599 }
2600 }
2601 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
2602 if(!ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
2603 continue;
2604 }
2605 }
2606 else
2607 {
2608 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
2609 if(!ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
2610 continue;
2611 }
2612
2613 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
2614
2615 CUIRect Button;
2616 Slot.VSplitLeft(Cut: 12.0f, pLeft: nullptr, pRight: &Slot);
2617 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Button);
2618
2619 const int MouseClick = DoButton_FontIcon(pId: &Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible, pText: Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible ? FontIcon::EYE : FontIcon::EYE_SLASH, Checked: 0, pRect: &VisibleToggle, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Left click to toggle visibility. Right click to show only this layer within its group.", Corners: IGraphics::CORNER_L, FontSize: 8.0f);
2620 if(MouseClick == 1)
2621 {
2622 Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible = !Map()->m_vpGroups[g]->m_vpLayers[i]->m_Visible;
2623 }
2624 else if(MouseClick == 2)
2625 {
2626 if(Input()->ShiftIsPressed())
2627 {
2628 if(!IsLayerSelected)
2629 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2630 }
2631
2632 int NumActive = 0;
2633 for(auto &Layer : Map()->m_vpGroups[g]->m_vpLayers)
2634 {
2635 if(Layer == Map()->m_vpGroups[g]->m_vpLayers[i])
2636 {
2637 Layer->m_Visible = true;
2638 continue;
2639 }
2640
2641 if(Layer->m_Visible)
2642 {
2643 Layer->m_Visible = false;
2644 NumActive++;
2645 }
2646 }
2647 if(NumActive == 0)
2648 {
2649 for(auto &Layer : Map()->m_vpGroups[g]->m_vpLayers)
2650 {
2651 Layer->m_Visible = true;
2652 }
2653 }
2654 }
2655
2656 if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_aName[0])
2657 str_copy(dst&: aBuf, src: Map()->m_vpGroups[g]->m_vpLayers[i]->m_aName);
2658 else
2659 {
2660 if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_TILES)
2661 {
2662 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
2663 str_copy(dst&: aBuf, src: pTiles->m_Image >= 0 ? Map()->m_vpImages[pTiles->m_Image]->m_aName : "Tiles");
2664 }
2665 else if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_QUADS)
2666 {
2667 std::shared_ptr<CLayerQuads> pQuads = std::static_pointer_cast<CLayerQuads>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
2668 str_copy(dst&: aBuf, src: pQuads->m_Image >= 0 ? Map()->m_vpImages[pQuads->m_Image]->m_aName : "Quads");
2669 }
2670 else if(Map()->m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_SOUNDS)
2671 {
2672 std::shared_ptr<CLayerSounds> pSounds = std::static_pointer_cast<CLayerSounds>(r: Map()->m_vpGroups[g]->m_vpLayers[i]);
2673 str_copy(dst&: aBuf, src: pSounds->m_Sound >= 0 ? Map()->m_vpSounds[pSounds->m_Sound]->m_aName : "Sounds");
2674 }
2675 }
2676
2677 int Checked = IsLayerSelected ? 1 : 0;
2678 if(Map()->m_vpGroups[g]->m_vpLayers[i]->IsEntitiesLayer())
2679 {
2680 Checked += 6;
2681 }
2682
2683 bool Clicked;
2684 bool Abrupted;
2685 if(int Result = DoButton_DraggableEx(pId: Map()->m_vpGroups[g]->m_vpLayers[i].get(), pText: aBuf, Checked, pRect: &Button, pClicked: &Clicked, pAbrupted: &Abrupted,
2686 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select layer. Hold shift to select multiple.", Corners: IGraphics::CORNER_R))
2687 {
2688 if(State.m_Operation == ELayerOperation::NONE)
2689 {
2690 State.m_InitialMouseY = Ui()->MouseY();
2691 State.m_InitialCutHeight = State.m_InitialMouseY - UnscrolledLayersBox.y;
2692
2693 SetOperation(ELayerOperation::CLICK);
2694
2695 if(!Input()->ShiftIsPressed() && !IsLayerSelected)
2696 {
2697 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2698 }
2699 }
2700
2701 if(Abrupted)
2702 {
2703 SetOperation(ELayerOperation::NONE);
2704 }
2705
2706 if(State.m_Operation == ELayerOperation::CLICK && absolute(a: Ui()->MouseY() - State.m_InitialMouseY) > MinDragDistance)
2707 {
2708 bool EntitiesLayerSelected = false;
2709 for(int k : Map()->m_vSelectedLayers)
2710 {
2711 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[k]->IsEntitiesLayer())
2712 EntitiesLayerSelected = true;
2713 }
2714
2715 if(!EntitiesLayerSelected)
2716 StartDragLayer = true;
2717
2718 State.m_pDraggedButton = Map()->m_vpGroups[g]->m_vpLayers[i].get();
2719 }
2720
2721 if(State.m_Operation == ELayerOperation::CLICK && Clicked)
2722 {
2723 State.m_LayerPopupContext.m_pEditor = this;
2724 if(Result == 1)
2725 {
2726 if(Input()->ShiftIsPressed() && Map()->m_SelectedGroup == g)
2727 {
2728 auto Position = std::find(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), val: i);
2729 if(Position != Map()->m_vSelectedLayers.end())
2730 Map()->m_vSelectedLayers.erase(position: Position);
2731 else
2732 Map()->AddSelectedLayer(LayerIndex: i);
2733 }
2734 else if(!Input()->ShiftIsPressed())
2735 {
2736 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2737 }
2738 }
2739 else if(Result == 2)
2740 {
2741 State.m_LayerPopupContext.m_vpLayers.clear();
2742 State.m_LayerPopupContext.m_vLayerIndices.clear();
2743
2744 if(!IsLayerSelected)
2745 {
2746 Map()->SelectLayer(LayerIndex: i, GroupIndex: g);
2747 }
2748
2749 if(Map()->m_vSelectedLayers.size() > 1)
2750 {
2751 // move right clicked layer to first index to render correct popup
2752 if(Map()->m_vSelectedLayers[0] != i)
2753 {
2754 auto Position = std::find(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), val: i);
2755 std::swap(a&: Map()->m_vSelectedLayers[0], b&: *Position);
2756 }
2757
2758 bool AllTile = true;
2759 for(size_t j = 0; AllTile && j < Map()->m_vSelectedLayers.size(); j++)
2760 {
2761 int LayerIndex = Map()->m_vSelectedLayers[j];
2762 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[LayerIndex]->m_Type == LAYERTYPE_TILES)
2763 {
2764 State.m_LayerPopupContext.m_vpLayers.push_back(x: std::static_pointer_cast<CLayerTiles>(r: Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers[Map()->m_vSelectedLayers[j]]));
2765 State.m_LayerPopupContext.m_vLayerIndices.push_back(x: LayerIndex);
2766 }
2767 else
2768 AllTile = false;
2769 }
2770
2771 // Don't allow editing if all selected layers are not tile layers
2772 if(!AllTile)
2773 {
2774 State.m_LayerPopupContext.m_vpLayers.clear();
2775 State.m_LayerPopupContext.m_vLayerIndices.clear();
2776 }
2777 }
2778
2779 Ui()->DoPopupMenu(pId: &State.m_LayerPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 150, Height: 300, pContext: &State.m_LayerPopupContext, pfnFunc: PopupLayer);
2780 }
2781
2782 SetOperation(ELayerOperation::NONE);
2783 }
2784
2785 if(State.m_Operation == ELayerOperation::LAYER_DRAG && Clicked)
2786 {
2787 MoveLayers = true;
2788 }
2789 }
2790 else if(State.m_pDraggedButton == Map()->m_vpGroups[g]->m_vpLayers[i].get())
2791 {
2792 SetOperation(ELayerOperation::NONE);
2793 }
2794 }
2795
2796 if(State.m_Operation != ELayerOperation::GROUP_DRAG || g != Map()->m_SelectedGroup)
2797 {
2798 LayersBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &LayersBox);
2799 ScrollRegion.AddRect(Rect: Slot);
2800 }
2801 }
2802
2803 if(!DraggedPositionFound && State.m_Operation == ELayerOperation::LAYER_DRAG)
2804 {
2805 GroupAfterDraggedLayer = Map()->m_vpGroups.size();
2806 LayerAfterDraggedLayer = Map()->m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers.size();
2807
2808 CUIRect TmpSlot;
2809 LayersBox.HSplitTop(Cut: Map()->m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &TmpSlot, pBottom: &LayersBox);
2810 ScrollRegion.AddRect(Rect: TmpSlot);
2811 }
2812
2813 if(!DraggedPositionFound && State.m_Operation == ELayerOperation::GROUP_DRAG)
2814 {
2815 GroupAfterDraggedLayer = Map()->m_vpGroups.size();
2816
2817 CUIRect TmpSlot;
2818 if(Map()->m_vpGroups[Map()->m_SelectedGroup]->m_Collapse)
2819 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2820 else
2821 LayersBox.HSplitTop(Cut: vButtonsPerGroup[Map()->m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
2822 ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
2823 }
2824
2825 if(MoveLayers && 1 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)Map()->m_vpGroups.size())
2826 {
2827 std::vector<std::shared_ptr<CLayer>> &vpNewGroupLayers = Map()->m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers;
2828 if(0 <= LayerAfterDraggedLayer && LayerAfterDraggedLayer <= (int)vpNewGroupLayers.size())
2829 {
2830 std::vector<std::shared_ptr<CLayer>> vpSelectedLayers;
2831 std::vector<std::shared_ptr<CLayer>> &vpSelectedGroupLayers = Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers;
2832 std::shared_ptr<CLayer> pNextLayer = nullptr;
2833 if(LayerAfterDraggedLayer < (int)vpNewGroupLayers.size())
2834 pNextLayer = vpNewGroupLayers[LayerAfterDraggedLayer];
2835
2836 std::sort(first: Map()->m_vSelectedLayers.begin(), last: Map()->m_vSelectedLayers.end(), comp: std::greater<>());
2837 for(int k : Map()->m_vSelectedLayers)
2838 {
2839 vpSelectedLayers.insert(position: vpSelectedLayers.begin(), x: vpSelectedGroupLayers[k]);
2840 }
2841 for(int k : Map()->m_vSelectedLayers)
2842 {
2843 vpSelectedGroupLayers.erase(position: vpSelectedGroupLayers.begin() + k);
2844 }
2845
2846 auto InsertPosition = std::find(first: vpNewGroupLayers.begin(), last: vpNewGroupLayers.end(), val: pNextLayer);
2847 int InsertPositionIndex = InsertPosition - vpNewGroupLayers.begin();
2848 vpNewGroupLayers.insert(position: InsertPosition, first: vpSelectedLayers.begin(), last: vpSelectedLayers.end());
2849
2850 int NumSelectedLayers = Map()->m_vSelectedLayers.size();
2851 Map()->m_vSelectedLayers.clear();
2852 for(int i = 0; i < NumSelectedLayers; i++)
2853 Map()->m_vSelectedLayers.push_back(x: InsertPositionIndex + i);
2854
2855 Map()->m_SelectedGroup = GroupAfterDraggedLayer - 1;
2856 Map()->OnModify();
2857 }
2858 }
2859
2860 if(MoveGroup && 0 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)Map()->m_vpGroups.size())
2861 {
2862 std::shared_ptr<CLayerGroup> pSelectedGroup = Map()->m_vpGroups[Map()->m_SelectedGroup];
2863 std::shared_ptr<CLayerGroup> pNextGroup = nullptr;
2864 if(GroupAfterDraggedLayer < (int)Map()->m_vpGroups.size())
2865 pNextGroup = Map()->m_vpGroups[GroupAfterDraggedLayer];
2866
2867 Map()->m_vpGroups.erase(position: Map()->m_vpGroups.begin() + Map()->m_SelectedGroup);
2868
2869 auto InsertPosition = std::find(first: Map()->m_vpGroups.begin(), last: Map()->m_vpGroups.end(), val: pNextGroup);
2870 Map()->m_vpGroups.insert(position: InsertPosition, x: pSelectedGroup);
2871
2872 auto Pos = std::find(first: Map()->m_vpGroups.begin(), last: Map()->m_vpGroups.end(), val: pSelectedGroup);
2873 Map()->m_SelectedGroup = Pos - Map()->m_vpGroups.begin();
2874
2875 Map()->OnModify();
2876 }
2877
2878 if(MoveLayers || MoveGroup)
2879 {
2880 SetOperation(ELayerOperation::NONE);
2881 }
2882 if(StartDragLayer)
2883 {
2884 SetOperation(ELayerOperation::LAYER_DRAG);
2885 State.m_InitialGroupIndex = Map()->m_SelectedGroup;
2886 State.m_vInitialLayerIndices = std::vector(Map()->m_vSelectedLayers);
2887 }
2888 if(StartDragGroup)
2889 {
2890 State.m_InitialGroupIndex = Map()->m_SelectedGroup;
2891 SetOperation(ELayerOperation::GROUP_DRAG);
2892 }
2893
2894 if(State.m_Operation == ELayerOperation::LAYER_DRAG || State.m_Operation == ELayerOperation::GROUP_DRAG)
2895 {
2896 if(State.m_pDraggedButton == nullptr)
2897 {
2898 SetOperation(ELayerOperation::NONE);
2899 }
2900 else
2901 {
2902 ScrollRegion.DoEdgeScrolling();
2903 Ui()->SetActiveItem(State.m_pDraggedButton);
2904 }
2905 }
2906
2907 if(Input()->KeyPress(Key: KEY_DOWN) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && State.m_Operation == ELayerOperation::NONE)
2908 {
2909 if(Input()->ShiftIsPressed())
2910 {
2911 if(Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] < (int)Map()->m_vpGroups[Map()->m_SelectedGroup]->m_vpLayers.size() - 1)
2912 Map()->AddSelectedLayer(LayerIndex: Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] + 1);
2913 }
2914 else
2915 {
2916 Map()->SelectNextLayer();
2917 }
2918 State.m_ScrollToSelectionNext = true;
2919 }
2920 if(Input()->KeyPress(Key: KEY_UP) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && State.m_Operation == ELayerOperation::NONE)
2921 {
2922 if(Input()->ShiftIsPressed())
2923 {
2924 if(Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] > 0)
2925 Map()->AddSelectedLayer(LayerIndex: Map()->m_vSelectedLayers[Map()->m_vSelectedLayers.size() - 1] - 1);
2926 }
2927 else
2928 {
2929 Map()->SelectPreviousLayer();
2930 }
2931
2932 State.m_ScrollToSelectionNext = true;
2933 }
2934
2935 CUIRect AddGroupButton, CollapseAllButton;
2936 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &AddGroupButton, pBottom: &LayersBox);
2937 if(ScrollRegion.AddRect(Rect: AddGroupButton))
2938 {
2939 AddGroupButton.HSplitTop(Cut: RowHeight, pTop: &AddGroupButton, pBottom: nullptr);
2940 if(DoButton_Editor(pId: &State.m_AddGroupButtonId, pText: m_QuickActionAddGroup.Label(), Checked: 0, pRect: &AddGroupButton, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddGroup.Description()))
2941 {
2942 m_QuickActionAddGroup.Call();
2943 }
2944 }
2945
2946 LayersBox.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &LayersBox);
2947 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &CollapseAllButton, pBottom: &LayersBox);
2948 if(ScrollRegion.AddRect(Rect: CollapseAllButton))
2949 {
2950 size_t TotalCollapsed = 0;
2951 for(const auto &pGroup : Map()->m_vpGroups)
2952 {
2953 if(pGroup->m_vpLayers.empty() || pGroup->m_Collapse)
2954 {
2955 TotalCollapsed++;
2956 }
2957 }
2958
2959 const char *pActionText = TotalCollapsed == Map()->m_vpGroups.size() ? "Expand all" : "Collapse all";
2960
2961 CollapseAllButton.HSplitTop(Cut: RowHeight, pTop: &CollapseAllButton, pBottom: nullptr);
2962 if(DoButton_Editor(pId: &State.m_CollapseAllButtonId, pText: pActionText, Checked: 0, pRect: &CollapseAllButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Expand or collapse all groups."))
2963 {
2964 for(const auto &pGroup : Map()->m_vpGroups)
2965 {
2966 if(TotalCollapsed == Map()->m_vpGroups.size())
2967 pGroup->m_Collapse = false;
2968 else if(!pGroup->m_vpLayers.empty())
2969 pGroup->m_Collapse = true;
2970 }
2971 }
2972 }
2973
2974 ScrollRegion.End();
2975
2976 if(State.m_Operation == ELayerOperation::NONE)
2977 {
2978 if(State.m_PreviousOperation == ELayerOperation::GROUP_DRAG)
2979 {
2980 State.m_PreviousOperation = ELayerOperation::NONE;
2981 Map()->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditGroupProp>(args: Map(), args&: Map()->m_SelectedGroup, args: EGroupProp::ORDER, args&: State.m_InitialGroupIndex, args&: Map()->m_SelectedGroup));
2982 }
2983 else if(State.m_PreviousOperation == ELayerOperation::LAYER_DRAG)
2984 {
2985 if(State.m_InitialGroupIndex != Map()->m_SelectedGroup)
2986 {
2987 Map()->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditLayersGroupAndOrder>(args: Map(), args&: State.m_InitialGroupIndex, args&: State.m_vInitialLayerIndices, args&: Map()->m_SelectedGroup, args&: Map()->m_vSelectedLayers));
2988 }
2989 else
2990 {
2991 std::vector<std::shared_ptr<IEditorAction>> vpActions;
2992 std::vector<int> vLayerIndices = Map()->m_vSelectedLayers;
2993 std::sort(first: vLayerIndices.begin(), last: vLayerIndices.end());
2994 std::sort(first: State.m_vInitialLayerIndices.begin(), last: State.m_vInitialLayerIndices.end());
2995 for(int k = 0; k < (int)vLayerIndices.size(); k++)
2996 {
2997 int LayerIndex = vLayerIndices[k];
2998 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerProp>(args: Map(), args&: Map()->m_SelectedGroup, args&: LayerIndex, args: ELayerProp::ORDER, args&: State.m_vInitialLayerIndices[k], args&: LayerIndex));
2999 }
3000 Map()->m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: Map(), args&: vpActions, args: nullptr, args: true));
3001 }
3002 State.m_PreviousOperation = ELayerOperation::NONE;
3003 }
3004 }
3005}
3006
3007bool CEditor::ReplaceImage(const char *pFilename, int StorageType, bool CheckDuplicate)
3008{
3009 // check if we have that image already
3010 char aBuf[IO_MAX_PATH_LENGTH];
3011 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
3012 if(CheckDuplicate)
3013 {
3014 for(const auto &pImage : Map()->m_vpImages)
3015 {
3016 if(!str_comp(a: pImage->m_aName, b: aBuf))
3017 {
3018 ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
3019 return false;
3020 }
3021 }
3022 }
3023
3024 CImageInfo ImgInfo;
3025 if(!Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
3026 {
3027 ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
3028 return false;
3029 }
3030
3031 std::shared_ptr<CEditorImage> pImg = Map()->SelectedImage();
3032 pImg->CEditorImage::Free();
3033 *pImg = std::move(ImgInfo);
3034 str_copy(dst&: pImg->m_aName, src: aBuf);
3035 pImg->m_External = IsVanillaImage(pImage: pImg->m_aName);
3036
3037 ConvertToRgba(Image&: *pImg);
3038 DilateImage(Image: *pImg);
3039
3040 pImg->m_Automapper.Load(pTileName: pImg->m_aName);
3041 int TextureLoadFlag = Graphics()->TextureLoadFlags();
3042 if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
3043 TextureLoadFlag = 0;
3044 pImg->m_Texture = Graphics()->LoadTextureRaw(Image: *pImg, Flags: TextureLoadFlag, pTexName: pFilename);
3045
3046 Map()->SortImages();
3047 Map()->SelectImage(pImage: pImg);
3048 OnDialogClose();
3049 return true;
3050}
3051
3052bool CEditor::ReplaceImageCallback(const char *pFilename, int StorageType, void *pUser)
3053{
3054 return static_cast<CEditor *>(pUser)->ReplaceImage(pFilename, StorageType, CheckDuplicate: true);
3055}
3056
3057bool CEditor::AddImage(const char *pFilename, int StorageType, void *pUser)
3058{
3059 CEditor *pEditor = (CEditor *)pUser;
3060
3061 // check if we have that image already
3062 char aBuf[IO_MAX_PATH_LENGTH];
3063 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
3064 for(const auto &pImage : pEditor->Map()->m_vpImages)
3065 {
3066 if(!str_comp(a: pImage->m_aName, b: aBuf))
3067 {
3068 pEditor->ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
3069 return false;
3070 }
3071 }
3072
3073 if(pEditor->Map()->m_vpImages.size() >= MAX_MAPIMAGES)
3074 {
3075 pEditor->m_PopupEventType = POPEVENT_IMAGE_MAX;
3076 pEditor->m_PopupEventActivated = true;
3077 return false;
3078 }
3079
3080 CImageInfo ImgInfo;
3081 if(!pEditor->Graphics()->LoadPng(Image&: ImgInfo, pFilename, StorageType))
3082 {
3083 pEditor->ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFilename);
3084 return false;
3085 }
3086
3087 std::shared_ptr<CEditorImage> pImg = std::make_shared<CEditorImage>(args: pEditor->Map());
3088 *pImg = std::move(ImgInfo);
3089
3090 pImg->m_External = IsVanillaImage(pImage: aBuf);
3091
3092 ConvertToRgba(Image&: *pImg);
3093 DilateImage(Image: *pImg);
3094
3095 int TextureLoadFlag = pEditor->Graphics()->TextureLoadFlags();
3096 if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0)
3097 TextureLoadFlag = 0;
3098 pImg->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image: *pImg, Flags: TextureLoadFlag, pTexName: pFilename);
3099 str_copy(dst&: pImg->m_aName, src: aBuf);
3100 pImg->m_Automapper.Load(pTileName: pImg->m_aName);
3101 pEditor->Map()->m_vpImages.push_back(x: pImg);
3102 pEditor->Map()->SortImages();
3103 pEditor->Map()->SelectImage(pImage: pImg);
3104 pEditor->OnDialogClose();
3105 return true;
3106}
3107
3108bool CEditor::AddSound(const char *pFilename, int StorageType, void *pUser)
3109{
3110 CEditor *pEditor = (CEditor *)pUser;
3111
3112 // check if we have that sound already
3113 char aBuf[IO_MAX_PATH_LENGTH];
3114 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
3115 for(const auto &pSound : pEditor->Map()->m_vpSounds)
3116 {
3117 if(!str_comp(a: pSound->m_aName, b: aBuf))
3118 {
3119 pEditor->ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
3120 return false;
3121 }
3122 }
3123
3124 if(pEditor->Map()->m_vpSounds.size() >= MAX_MAPSOUNDS)
3125 {
3126 pEditor->m_PopupEventType = POPEVENT_SOUND_MAX;
3127 pEditor->m_PopupEventActivated = true;
3128 return false;
3129 }
3130
3131 // load external
3132 void *pData;
3133 unsigned DataSize;
3134 if(!pEditor->Storage()->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
3135 {
3136 pEditor->ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFilename);
3137 return false;
3138 }
3139
3140 // load sound
3141 const int SoundId = pEditor->Sound()->LoadOpusFromMem(pData, DataSize, ForceLoad: true, pContextName: pFilename);
3142 if(SoundId == -1)
3143 {
3144 free(ptr: pData);
3145 pEditor->ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFilename);
3146 return false;
3147 }
3148
3149 // add sound
3150 std::shared_ptr<CEditorSound> pSound = std::make_shared<CEditorSound>(args: pEditor->Map());
3151 pSound->m_SoundId = SoundId;
3152 pSound->m_DataSize = DataSize;
3153 pSound->m_pData = pData;
3154 str_copy(dst&: pSound->m_aName, src: aBuf);
3155 pEditor->Map()->m_vpSounds.push_back(x: pSound);
3156
3157 pEditor->Map()->SelectSound(pSound);
3158 pEditor->OnDialogClose();
3159 return true;
3160}
3161
3162bool CEditor::ReplaceSound(const char *pFilename, int StorageType, bool CheckDuplicate)
3163{
3164 // check if we have that sound already
3165 char aBuf[IO_MAX_PATH_LENGTH];
3166 fs_split_file_extension(filename: fs_filename(path: pFilename), name: aBuf, name_size: sizeof(aBuf));
3167 if(CheckDuplicate)
3168 {
3169 for(const auto &pSound : Map()->m_vpSounds)
3170 {
3171 if(!str_comp(a: pSound->m_aName, b: aBuf))
3172 {
3173 ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
3174 return false;
3175 }
3176 }
3177 }
3178
3179 // load external
3180 void *pData;
3181 unsigned DataSize;
3182 if(!Storage()->ReadFile(pFilename, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
3183 {
3184 ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFilename);
3185 return false;
3186 }
3187
3188 // load sound
3189 const int SoundId = Sound()->LoadOpusFromMem(pData, DataSize, ForceLoad: true, pContextName: pFilename);
3190 if(SoundId == -1)
3191 {
3192 free(ptr: pData);
3193 ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFilename);
3194 return false;
3195 }
3196
3197 std::shared_ptr<CEditorSound> pSound = Map()->SelectedSound();
3198
3199 if(m_ToolbarPreviewSound == pSound->m_SoundId)
3200 {
3201 m_ToolbarPreviewSound = SoundId;
3202 }
3203
3204 // unload sample
3205 Sound()->UnloadSample(SampleId: pSound->m_SoundId);
3206 free(ptr: pSound->m_pData);
3207
3208 // replace sound
3209 str_copy(dst&: pSound->m_aName, src: aBuf);
3210 pSound->m_SoundId = SoundId;
3211 pSound->m_pData = pData;
3212 pSound->m_DataSize = DataSize;
3213
3214 Map()->SelectSound(pSound);
3215 OnDialogClose();
3216 return true;
3217}
3218
3219bool CEditor::ReplaceSoundCallback(const char *pFilename, int StorageType, void *pUser)
3220{
3221 return static_cast<CEditor *>(pUser)->ReplaceSound(pFilename, StorageType, CheckDuplicate: true);
3222}
3223
3224void CEditor::RenderImagesList(CUIRect ToolBox)
3225{
3226 const float RowHeight = 12.0f;
3227
3228 CScrollRegion &ScrollRegion = Map()->m_EditorUiElements.m_ImagesScrollRegion;
3229 CScrollRegionParams ScrollParams;
3230 ScrollParams.m_ScrollbarThickness = 10.0f;
3231 ScrollParams.m_ScrollbarMargin = 3.0f;
3232 ScrollParams.m_ScrollUnit = RowHeight * 5;
3233 ScrollRegion.Begin(pClipRect: &ToolBox, pParams: &ScrollParams);
3234
3235 bool ScrollToSelection = false;
3236 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Map()->m_vpImages.empty())
3237 {
3238 if(Input()->KeyPress(Key: KEY_DOWN))
3239 {
3240 const int OldImage = Map()->m_SelectedImage;
3241 Map()->SelectNextImage();
3242 ScrollToSelection = OldImage != Map()->m_SelectedImage;
3243 }
3244 else if(Input()->KeyPress(Key: KEY_UP))
3245 {
3246 const int OldImage = Map()->m_SelectedImage;
3247 Map()->SelectPreviousImage();
3248 ScrollToSelection = OldImage != Map()->m_SelectedImage;
3249 }
3250 }
3251
3252 for(int e = 0; e < 2; e++) // two passes, first embedded, then external
3253 {
3254 CUIRect Slot;
3255 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
3256 if(ScrollRegion.AddRect(Rect: Slot))
3257 Ui()->DoLabel(pRect: &Slot, pText: e == 0 ? "Embedded" : "External", Size: 12.0f, Align: TEXTALIGN_MC);
3258
3259 for(int i = 0; i < (int)Map()->m_vpImages.size(); i++)
3260 {
3261 if((e && !Map()->m_vpImages[i]->m_External) ||
3262 (!e && Map()->m_vpImages[i]->m_External))
3263 {
3264 continue;
3265 }
3266
3267 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
3268 int Selected = Map()->m_SelectedImage == i;
3269 if(!ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
3270 continue;
3271 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
3272
3273 const bool ImageUsed = std::any_of(first: Map()->m_vpGroups.cbegin(), last: Map()->m_vpGroups.cend(), pred: [i](const auto &pGroup) {
3274 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
3275 if(pLayer->m_Type == LAYERTYPE_QUADS)
3276 return std::static_pointer_cast<CLayerQuads>(pLayer)->m_Image == i;
3277 else if(pLayer->m_Type == LAYERTYPE_TILES)
3278 return std::static_pointer_cast<CLayerTiles>(pLayer)->m_Image == i;
3279 return false;
3280 });
3281 });
3282
3283 if(!ImageUsed)
3284 Selected += 2; // Image is unused
3285
3286 if(Selected < 2 && e == 1)
3287 {
3288 if(!IsVanillaImage(pImage: Map()->m_vpImages[i]->m_aName))
3289 {
3290 Selected += 4; // Image should be embedded
3291 }
3292 }
3293
3294 if(int Result = DoButton_Ex(pId: &Map()->m_vpImages[i], pText: Map()->m_vpImages[i]->m_aName, Checked: Selected, pRect: &Slot,
3295 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select image.", Corners: IGraphics::CORNER_ALL))
3296 {
3297 Map()->m_SelectedImage = i;
3298
3299 if(Result == 2)
3300 {
3301 const int Height = Map()->SelectedImage()->m_External ? 73 : 107;
3302 static SPopupMenuId s_PopupImageId;
3303 Ui()->DoPopupMenu(pId: &s_PopupImageId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height, pContext: this, pfnFunc: PopupImage);
3304 }
3305 }
3306 }
3307
3308 // separator
3309 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
3310 if(ScrollRegion.AddRect(Rect: Slot))
3311 {
3312 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
3313 Graphics()->TextureClear();
3314 Graphics()->LinesBegin();
3315 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
3316 Graphics()->LinesEnd();
3317 }
3318 }
3319
3320 // new image
3321 static int s_AddImageButton = 0;
3322 CUIRect AddImageButton;
3323 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddImageButton, pBottom: &ToolBox);
3324 if(ScrollRegion.AddRect(Rect: AddImageButton))
3325 {
3326 AddImageButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddImageButton);
3327 AddImageButton.HSplitTop(Cut: RowHeight, pTop: &AddImageButton, pBottom: nullptr);
3328 if(DoButton_Editor(pId: &s_AddImageButton, pText: m_QuickActionAddImage.Label(), Checked: 0, pRect: &AddImageButton, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionAddImage.Description()))
3329 m_QuickActionAddImage.Call();
3330 }
3331 ScrollRegion.End();
3332}
3333
3334void CEditor::RenderSelectedImage(CUIRect View) const
3335{
3336 std::shared_ptr<CEditorImage> pSelectedImage = Map()->SelectedImage();
3337 if(pSelectedImage == nullptr)
3338 return;
3339
3340 View.Margin(Cut: 10.0f, pOtherRect: &View);
3341 if(View.h < View.w)
3342 View.w = View.h;
3343 else
3344 View.h = View.w;
3345 float Max = std::max(a: pSelectedImage->m_Width, b: pSelectedImage->m_Height);
3346 View.w *= pSelectedImage->m_Width / Max;
3347 View.h *= pSelectedImage->m_Height / Max;
3348 Graphics()->TextureSet(Texture: pSelectedImage->m_Texture);
3349 Graphics()->WrapClamp();
3350 Graphics()->QuadsBegin();
3351 IGraphics::CQuadItem QuadItem(View.x, View.y, View.w, View.h);
3352 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
3353 Graphics()->QuadsEnd();
3354 Graphics()->WrapNormal();
3355}
3356
3357void CEditor::RenderSounds(CUIRect ToolBox)
3358{
3359 const float RowHeight = 12.0f;
3360
3361 CScrollRegion &ScrollRegion = Map()->m_EditorUiElements.m_SoundsScrollRegion;
3362 CScrollRegionParams ScrollParams;
3363 ScrollParams.m_ScrollbarThickness = 10.0f;
3364 ScrollParams.m_ScrollbarMargin = 3.0f;
3365 ScrollParams.m_ScrollUnit = RowHeight * 5;
3366 ScrollRegion.Begin(pClipRect: &ToolBox, pParams: &ScrollParams);
3367
3368 bool ScrollToSelection = false;
3369 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !Map()->m_vpSounds.empty())
3370 {
3371 if(Input()->KeyPress(Key: KEY_DOWN))
3372 {
3373 Map()->SelectNextSound();
3374 ScrollToSelection = true;
3375 }
3376 else if(Input()->KeyPress(Key: KEY_UP))
3377 {
3378 Map()->SelectPreviousSound();
3379 ScrollToSelection = true;
3380 }
3381 }
3382
3383 CUIRect Slot;
3384 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
3385 if(ScrollRegion.AddRect(Rect: Slot))
3386 Ui()->DoLabel(pRect: &Slot, pText: "Embedded", Size: 12.0f, Align: TEXTALIGN_MC);
3387
3388 for(int i = 0; i < (int)Map()->m_vpSounds.size(); i++)
3389 {
3390 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
3391 int Selected = Map()->m_SelectedSound == i;
3392 if(!ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
3393 continue;
3394 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
3395
3396 const bool SoundUsed = std::any_of(first: Map()->m_vpGroups.cbegin(), last: Map()->m_vpGroups.cend(), pred: [i](const auto &pGroup) {
3397 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
3398 if(pLayer->m_Type == LAYERTYPE_SOUNDS)
3399 return std::static_pointer_cast<CLayerSounds>(pLayer)->m_Sound == i;
3400 return false;
3401 });
3402 });
3403
3404 if(!SoundUsed)
3405 Selected += 2; // Sound is unused
3406
3407 if(int Result = DoButton_Ex(pId: &Map()->m_vpSounds[i], pText: Map()->m_vpSounds[i]->m_aName, Checked: Selected, pRect: &Slot,
3408 Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT, pToolTip: "Select sound.", Corners: IGraphics::CORNER_ALL))
3409 {
3410 Map()->m_SelectedSound = i;
3411
3412 if(Result == 2)
3413 {
3414 static SPopupMenuId s_PopupSoundId;
3415 Ui()->DoPopupMenu(pId: &s_PopupSoundId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height: 90, pContext: this, pfnFunc: PopupSound);
3416 }
3417 }
3418 }
3419
3420 // separator
3421 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
3422 if(ScrollRegion.AddRect(Rect: Slot))
3423 {
3424 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
3425 Graphics()->TextureClear();
3426 Graphics()->LinesBegin();
3427 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
3428 Graphics()->LinesEnd();
3429 }
3430
3431 // new sound
3432 static int s_AddSoundButton = 0;
3433 CUIRect AddSoundButton;
3434 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddSoundButton, pBottom: &ToolBox);
3435 if(ScrollRegion.AddRect(Rect: AddSoundButton))
3436 {
3437 AddSoundButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddSoundButton);
3438 AddSoundButton.HSplitTop(Cut: RowHeight, pTop: &AddSoundButton, pBottom: nullptr);
3439 if(DoButton_Editor(pId: &s_AddSoundButton, pText: "Add sound", Checked: 0, pRect: &AddSoundButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Load a new sound to use in the map."))
3440 m_FileBrowser.ShowFileDialog(StorageType: IStorage::TYPE_ALL, FileType: CFileBrowser::EFileType::SOUND, pTitle: "Add sound", pButtonText: "Add", pInitialPath: "mapres", pInitialFilename: "", pfnOpenCallback: AddSound, pOpenCallbackUser: this);
3441 }
3442 ScrollRegion.End();
3443}
3444
3445bool CEditor::CStringKeyComparator::operator()(const char *pLhs, const char *pRhs) const
3446{
3447 return str_comp(a: pLhs, b: pRhs) < 0;
3448}
3449
3450void CEditor::ShowFileDialogError(const char *pFormat, ...)
3451{
3452 char aMessage[1024];
3453 va_list VarArgs;
3454 va_start(VarArgs, pFormat);
3455 str_format_v(buffer: aMessage, buffer_size: sizeof(aMessage), format: pFormat, args: VarArgs);
3456 va_end(VarArgs);
3457
3458 auto ContextIterator = m_PopupMessageContexts.find(x: aMessage);
3459 CUi::SMessagePopupContext *pContext;
3460 if(ContextIterator != m_PopupMessageContexts.end())
3461 {
3462 pContext = ContextIterator->second;
3463 Ui()->ClosePopupMenu(pId: pContext);
3464 }
3465 else
3466 {
3467 pContext = new CUi::SMessagePopupContext();
3468 pContext->ErrorColor();
3469 str_copy(dst&: pContext->m_aMessage, src: aMessage);
3470 m_PopupMessageContexts[pContext->m_aMessage] = pContext;
3471 }
3472 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext);
3473}
3474
3475void CEditor::RenderModebar(CUIRect View)
3476{
3477 CUIRect Mentions, IngameMoved, ModeButtons, ModeButton;
3478 View.HSplitTop(Cut: 12.0f, pTop: &Mentions, pBottom: &View);
3479 View.HSplitTop(Cut: 12.0f, pTop: &IngameMoved, pBottom: &View);
3480 View.HSplitBottom(Cut: 22.0f, pTop: nullptr, pBottom: &ModeButtons);
3481 const float Width = m_ToolBoxWidth - 5.0f;
3482 ModeButtons.VSplitLeft(Cut: Width, pLeft: &ModeButtons, pRight: nullptr);
3483 const float ButtonWidth = Width / 3;
3484
3485 // mentions
3486 if(m_Mentions)
3487 {
3488 char aBuf[64];
3489 if(m_Mentions == 1)
3490 str_copy(dst&: aBuf, src: Localize(pStr: "1 new mention"));
3491 else if(m_Mentions <= 9)
3492 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d new mentions"), m_Mentions);
3493 else
3494 str_copy(dst&: aBuf, src: Localize(pStr: "9+ new mentions"));
3495
3496 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
3497 Ui()->DoLabel(pRect: &Mentions, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
3498 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
3499 }
3500
3501 // ingame moved warning
3502 if(m_IngameMoved)
3503 {
3504 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
3505 Ui()->DoLabel(pRect: &IngameMoved, pText: Localize(pStr: "Moved ingame"), Size: 10.0f, Align: TEXTALIGN_MC);
3506 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
3507 }
3508
3509 // mode buttons
3510 {
3511 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
3512 static int s_LayersButton = 0;
3513 if(DoButton_FontIcon(pId: &s_LayersButton, pText: FontIcon::LAYER_GROUP, Checked: m_Mode == MODE_LAYERS, pRect: &ModeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Go to layers management.", Corners: IGraphics::CORNER_L))
3514 {
3515 m_Mode = MODE_LAYERS;
3516 }
3517
3518 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
3519 static int s_ImagesButton = 0;
3520 if(DoButton_FontIcon(pId: &s_ImagesButton, pText: FontIcon::IMAGE, Checked: m_Mode == MODE_IMAGES, pRect: &ModeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Go to images management.", Corners: IGraphics::CORNER_NONE))
3521 {
3522 m_Mode = MODE_IMAGES;
3523 }
3524
3525 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
3526 static int s_SoundsButton = 0;
3527 if(DoButton_FontIcon(pId: &s_SoundsButton, pText: FontIcon::MUSIC, Checked: m_Mode == MODE_SOUNDS, pRect: &ModeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Go to sounds management.", Corners: IGraphics::CORNER_R))
3528 {
3529 m_Mode = MODE_SOUNDS;
3530 }
3531
3532 if(Input()->KeyPress(Key: KEY_LEFT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3533 {
3534 m_Mode = (m_Mode + NUM_MODES - 1) % NUM_MODES;
3535 }
3536 else if(Input()->KeyPress(Key: KEY_RIGHT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3537 {
3538 m_Mode = (m_Mode + 1) % NUM_MODES;
3539 }
3540 }
3541}
3542
3543void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect)
3544{
3545 CUIRect Button;
3546 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
3547 if(DoButton_Editor(pId: &m_QuickActionEnvelopes, pText: m_QuickActionEnvelopes.Label(), Checked: m_QuickActionEnvelopes.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionEnvelopes.Description()))
3548 {
3549 m_QuickActionEnvelopes.Call();
3550 }
3551
3552 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
3553 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
3554 if(DoButton_Editor(pId: &m_QuickActionServerSettings, pText: m_QuickActionServerSettings.Label(), Checked: m_QuickActionServerSettings.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionServerSettings.Description()))
3555 {
3556 m_QuickActionServerSettings.Call();
3557 }
3558
3559 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
3560 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
3561 if(DoButton_Editor(pId: &m_QuickActionHistory, pText: m_QuickActionHistory.Label(), Checked: m_QuickActionHistory.Color(), pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: m_QuickActionHistory.Description()))
3562 {
3563 m_QuickActionHistory.Call();
3564 }
3565
3566 View.VSplitRight(Cut: 10.0f, pLeft: pTooltipRect, pRight: nullptr);
3567}
3568
3569void CEditor::RenderTooltip(CUIRect TooltipRect)
3570{
3571 if(str_comp(a: m_aTooltip, b: "") == 0)
3572 return;
3573
3574 char aBuf[256];
3575 if(m_pUiGotContext && m_pUiGotContext == Ui()->HotItem())
3576 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s Right click for context menu.", m_aTooltip);
3577 else
3578 str_copy(dst&: aBuf, src: m_aTooltip);
3579
3580 SLabelProperties Props;
3581 Props.m_MaxWidth = TooltipRect.w;
3582 Props.m_EllipsisAtEnd = true;
3583 Ui()->DoLabel(pRect: &TooltipRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
3584}
3585
3586void CEditor::DoEditorDragBar(CUIRect View, CUIRect *pDragBar, EDragSide Side, float *pValue, float MinValue, float MaxValue)
3587{
3588 enum EDragOperation
3589 {
3590 OP_NONE,
3591 OP_DRAGGING,
3592 OP_CLICKED
3593 };
3594 static EDragOperation s_Operation = OP_NONE;
3595 static float s_InitialMouseY = 0.0f;
3596 static float s_InitialMouseOffsetY = 0.0f;
3597 static float s_InitialMouseX = 0.0f;
3598 static float s_InitialMouseOffsetX = 0.0f;
3599
3600 bool IsVertical = Side == EDragSide::TOP || Side == EDragSide::BOTTOM;
3601
3602 if(Ui()->MouseInside(pRect: pDragBar) && Ui()->HotItem() == pDragBar)
3603 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
3604
3605 bool Clicked;
3606 bool Abrupted;
3607 if(int Result = DoButton_DraggableEx(pId: pDragBar, pText: "", Checked: 8, pRect: pDragBar, pClicked: &Clicked, pAbrupted: &Abrupted, Flags: 0, pToolTip: "Change the size of the editor by dragging."))
3608 {
3609 if(s_Operation == OP_NONE && Result == 1)
3610 {
3611 s_InitialMouseY = Ui()->MouseY();
3612 s_InitialMouseOffsetY = Ui()->MouseY() - pDragBar->y;
3613 s_InitialMouseX = Ui()->MouseX();
3614 s_InitialMouseOffsetX = Ui()->MouseX() - pDragBar->x;
3615 s_Operation = OP_CLICKED;
3616 }
3617
3618 if(Clicked || Abrupted)
3619 s_Operation = OP_NONE;
3620
3621 if(s_Operation == OP_CLICKED && absolute(a: IsVertical ? Ui()->MouseY() - s_InitialMouseY : Ui()->MouseX() - s_InitialMouseX) > 5.0f)
3622 s_Operation = OP_DRAGGING;
3623
3624 if(s_Operation == OP_DRAGGING)
3625 {
3626 if(Side == EDragSide::TOP)
3627 *pValue = std::clamp(val: s_InitialMouseOffsetY + View.y + View.h - Ui()->MouseY(), lo: MinValue, hi: MaxValue);
3628 else if(Side == EDragSide::RIGHT)
3629 *pValue = std::clamp(val: Ui()->MouseX() - s_InitialMouseOffsetX - View.x + pDragBar->w, lo: MinValue, hi: MaxValue);
3630 else if(Side == EDragSide::BOTTOM)
3631 *pValue = std::clamp(val: Ui()->MouseY() - s_InitialMouseOffsetY - View.y + pDragBar->h, lo: MinValue, hi: MaxValue);
3632 else if(Side == EDragSide::LEFT)
3633 *pValue = std::clamp(val: s_InitialMouseOffsetX + View.x + View.w - Ui()->MouseX(), lo: MinValue, hi: MaxValue);
3634
3635 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
3636 }
3637 }
3638}
3639
3640void CEditor::RenderMenubar(CUIRect MenuBar)
3641{
3642 SPopupMenuProperties PopupProperties;
3643 PopupProperties.m_Corners = IGraphics::CORNER_R | IGraphics::CORNER_B;
3644
3645 CUIRect FileButton;
3646 static int s_FileButton = 0;
3647 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &FileButton, pRight: &MenuBar);
3648 if(DoButton_Ex(pId: &s_FileButton, pText: "File", Checked: 0, pRect: &FileButton, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
3649 {
3650 static SPopupMenuId s_PopupMenuFileId;
3651 Ui()->DoPopupMenu(pId: &s_PopupMenuFileId, X: FileButton.x, Y: FileButton.y + FileButton.h - 1.0f, Width: 120.0f, Height: 210.0f, pContext: this, pfnFunc: PopupMenuFile, Props: PopupProperties);
3652 }
3653
3654 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
3655
3656 CUIRect ToolsButton;
3657 static int s_ToolsButton = 0;
3658 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &ToolsButton, pRight: &MenuBar);
3659 if(DoButton_Ex(pId: &s_ToolsButton, pText: "Tools", Checked: 0, pRect: &ToolsButton, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
3660 {
3661 static SPopupMenuId s_PopupMenuToolsId;
3662 Ui()->DoPopupMenu(pId: &s_PopupMenuToolsId, X: ToolsButton.x, Y: ToolsButton.y + ToolsButton.h - 1.0f, Width: 200.0f, Height: 78.0f, pContext: this, pfnFunc: PopupMenuTools, Props: PopupProperties);
3663 }
3664
3665 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
3666
3667 CUIRect SettingsButton;
3668 static int s_SettingsButton = 0;
3669 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &SettingsButton, pRight: &MenuBar);
3670 if(DoButton_Ex(pId: &s_SettingsButton, pText: "Settings", Checked: 0, pRect: &SettingsButton, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
3671 {
3672 static SPopupMenuId s_PopupMenuSettingsId;
3673 Ui()->DoPopupMenu(pId: &s_PopupMenuSettingsId, X: SettingsButton.x, Y: SettingsButton.y + SettingsButton.h - 1.0f, Width: 280.0f, Height: 148.0f, pContext: this, pfnFunc: PopupMenuSettings, Props: PopupProperties);
3674 }
3675
3676 CUIRect Info, Help, Close;
3677 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
3678 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Close);
3679 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
3680 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Help);
3681 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &Info, pRight: nullptr);
3682
3683 char aTimeStr[6];
3684 str_timestamp_format(buffer: aTimeStr, buffer_size: sizeof(aTimeStr), format: "%H:%M");
3685
3686 char aBuf[256];
3687 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "X: %.1f, Y: %.1f, Z: %.1f, T: %.1f, A: %.1f, G: %i %s", MapView()->MouseWorldPos().x / 32.0f, MapView()->MouseWorldPos().y / 32.0f, MapView()->Zoom()->GetValue(), Map()->m_EnvelopeEvaluator.m_AnimateTime * Map()->m_EnvelopeEvaluator.m_AnimateSpeed, Map()->m_EnvelopeEvaluator.m_AnimateSpeed, MapView()->MapGrid()->Factor(), aTimeStr);
3688 Ui()->DoLabel(pRect: &Info, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MR);
3689
3690 static int s_HelpButton = 0;
3691 if(DoButton_Editor(pId: &s_HelpButton, pText: "?", Checked: 0, pRect: &Help, Flags: BUTTONFLAG_LEFT, pToolTip: "[F1] Open the DDNet Wiki page for the map editor in a web browser."))
3692 {
3693 m_QuickActionShowHelp.Call();
3694 }
3695
3696 static int s_CloseButton = 0;
3697 if(DoButton_Editor(pId: &s_CloseButton, pText: "×", Checked: 0, pRect: &Close, Flags: BUTTONFLAG_LEFT, pToolTip: "[Escape] Exit from the editor."))
3698 {
3699 OnClose();
3700 g_Config.m_ClEditor = 0;
3701 }
3702}
3703
3704void CEditor::ShowHelp()
3705{
3706 const char *pLink = Localize(pStr: "https://wiki.ddnet.org/wiki/Mapping");
3707 if(!Client()->ViewLink(pLink))
3708 {
3709 ShowFileDialogError(pFormat: "Failed to open the link '%s' in the default web browser.", pLink);
3710 }
3711}
3712
3713void CEditor::Exit()
3714{
3715 if(HasUnsavedData())
3716 {
3717 m_PopupEventType = POPEVENT_EXIT;
3718 m_PopupEventActivated = true;
3719 }
3720 else
3721 {
3722 OnClose();
3723 g_Config.m_ClEditor = 0;
3724 }
3725}
3726
3727void CEditor::Render()
3728{
3729 // basic start
3730 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
3731 CUIRect View = *Ui()->Screen();
3732 Ui()->MapScreen();
3733 m_CursorType = CURSOR_NORMAL;
3734
3735 float Width = View.w;
3736 float Height = View.h;
3737
3738 // reset tip
3739 str_copy(dst&: m_aTooltip, src: "");
3740
3741 // render checker
3742 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 1.0f);
3743
3744 UpdateBrushPicker();
3745
3746 CUIRect MenuBar, ModeBar, ToolBar, StatusBar, ExtraEditor, ToolBox;
3747 if(m_GuiActive)
3748 {
3749 View.HSplitTop(Cut: 20.0f, pTop: &MenuBar, pBottom: &View);
3750 View.HSplitTop(Cut: 78.0f, pTop: &ToolBar, pBottom: &View);
3751 View.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ToolBox, pRight: &View);
3752
3753 View.HSplitBottom(Cut: 16.0f, pTop: &View, pBottom: &StatusBar);
3754 if(!m_ShowPicker && m_ActiveExtraEditor != EXTRAEDITOR_NONE)
3755 View.HSplitBottom(Cut: m_aExtraEditorSplits[(int)m_ActiveExtraEditor], pTop: &View, pBottom: &ExtraEditor);
3756 }
3757 else
3758 {
3759 // hack to get keyboard inputs from toolbar even when GUI is not active
3760 ToolBar.x = -100;
3761 ToolBar.y = -100;
3762 ToolBar.w = 50;
3763 ToolBar.h = 50;
3764 }
3765
3766 // a little hack for now
3767 if(m_Mode == MODE_LAYERS)
3768 MapView()->Render(View);
3769
3770 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3771 {
3772 // handle undo/redo hotkeys
3773 if(Ui()->CheckActiveItem(pId: nullptr))
3774 {
3775 if(Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && !Input()->ShiftIsPressed())
3776 ActiveHistory().Undo();
3777 if((Input()->KeyPress(Key: KEY_Y) && Input()->ModifierIsPressed()) || (Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && Input()->ShiftIsPressed()))
3778 ActiveHistory().Redo();
3779 }
3780
3781 // handle brush save/load hotkeys
3782 for(int i = KEY_1; i <= KEY_0; i++)
3783 {
3784 if(Input()->KeyPress(Key: i))
3785 {
3786 int Slot = i - KEY_1;
3787 if(Input()->ModifierIsPressed() && !m_pBrush->IsEmpty())
3788 {
3789 dbg_msg(sys: "editor", fmt: "saving current brush to %d", Slot);
3790 m_apSavedBrushes[Slot] = std::make_shared<CLayerGroup>(args&: *m_pBrush);
3791 }
3792 else if(m_apSavedBrushes[Slot])
3793 {
3794 dbg_msg(sys: "editor", fmt: "loading brush from slot %d", Slot);
3795 m_pBrush = std::make_shared<CLayerGroup>(args&: *m_apSavedBrushes[Slot]);
3796 }
3797 }
3798 }
3799 }
3800
3801 const float BackgroundBrightness = 0.26f;
3802 const float BackgroundScale = 80.0f;
3803
3804 if(m_GuiActive)
3805 {
3806 RenderBackground(View: MenuBar, Texture: IGraphics::CTextureHandle(), Size: BackgroundScale, Brightness: 0.0f);
3807 MenuBar.Margin(Cut: 2.0f, pOtherRect: &MenuBar);
3808
3809 RenderBackground(View: ToolBox, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3810 ToolBox.Margin(Cut: 2.0f, pOtherRect: &ToolBox);
3811
3812 RenderBackground(View: ToolBar, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3813 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
3814 ToolBar.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ModeBar, pRight: &ToolBar);
3815
3816 RenderBackground(View: StatusBar, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3817 StatusBar.Margin(Cut: 2.0f, pOtherRect: &StatusBar);
3818 }
3819
3820 // do the toolbar
3821 {
3822 CUIRect MapTabs;
3823 ToolBar.HSplitTop(Cut: 20.0f, pTop: &MapTabs, pBottom: &ToolBar);
3824 ToolBar.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ToolBar);
3825 DoMapTabs(MapTabs);
3826 if(m_Mode == MODE_LAYERS)
3827 DoToolbarLayers(ToolBar);
3828 else if(m_Mode == MODE_IMAGES)
3829 DoToolbarImages(ToolBar);
3830 else if(m_Mode == MODE_SOUNDS)
3831 DoToolbarSounds(ToolBar);
3832 }
3833
3834 if(m_Dialog == DIALOG_NONE)
3835 {
3836 const bool ModPressed = Input()->ModifierIsPressed();
3837 const bool ShiftPressed = Input()->ShiftIsPressed();
3838 const bool AltPressed = Input()->AltIsPressed();
3839
3840 if(CLineInput::GetActiveInput() == nullptr)
3841 {
3842 // ctrl+a to append map
3843 if(Input()->KeyPress(Key: KEY_A) && ModPressed)
3844 {
3845 m_QuickActionAppendMap.Call();
3846 }
3847 }
3848
3849 // ctrl+n to create new map
3850 if(Input()->KeyPress(Key: KEY_N) && ModPressed)
3851 {
3852 m_QuickActionNewMap.Call();
3853 }
3854 // ctrl+o or ctrl+l to open
3855 if((Input()->KeyPress(Key: KEY_O) || Input()->KeyPress(Key: KEY_L)) && ModPressed)
3856 {
3857 if(ShiftPressed)
3858 {
3859 if(!m_QuickActionLoadIngameMap.Disabled())
3860 {
3861 m_QuickActionLoadIngameMap.Call();
3862 }
3863 }
3864 else
3865 {
3866 m_QuickActionLoadMap.Call();
3867 }
3868 }
3869
3870 // ctrl+shift+alt+s to save copy
3871 if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed && AltPressed)
3872 {
3873 m_QuickActionSaveCopy.Call();
3874 }
3875 // ctrl+shift+s to save as
3876 else if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed)
3877 {
3878 m_QuickActionSaveAs.Call();
3879 }
3880 // ctrl+s to save
3881 else if(Input()->KeyPress(Key: KEY_S) && ModPressed)
3882 {
3883 m_QuickActionSave.Call();
3884 }
3885 }
3886
3887 if(m_GuiActive)
3888 {
3889 CUIRect DragBar;
3890 ToolBox.VSplitRight(Cut: 1.0f, pLeft: &ToolBox, pRight: &DragBar);
3891 DragBar.x -= 2.0f;
3892 DragBar.w += 4.0f;
3893 DoEditorDragBar(View: ToolBox, pDragBar: &DragBar, Side: EDragSide::RIGHT, pValue: &m_ToolBoxWidth);
3894
3895 if(m_Mode == MODE_LAYERS)
3896 RenderLayers(LayersBox: ToolBox);
3897 else if(m_Mode == MODE_IMAGES)
3898 {
3899 RenderImagesList(ToolBox);
3900 RenderSelectedImage(View);
3901 }
3902 else if(m_Mode == MODE_SOUNDS)
3903 RenderSounds(ToolBox);
3904 }
3905
3906 Ui()->MapScreen();
3907
3908 CUIRect TooltipRect;
3909 if(m_GuiActive)
3910 {
3911 RenderMenubar(MenuBar);
3912 RenderModebar(View: ModeBar);
3913 if(!m_ShowPicker)
3914 {
3915 if(m_ActiveExtraEditor != EXTRAEDITOR_NONE)
3916 {
3917 RenderBackground(View: ExtraEditor, Texture: g_pData->m_aImages[IMAGE_BACKGROUND_NOISE].m_Id, Size: BackgroundScale, Brightness: BackgroundBrightness);
3918 ExtraEditor.HMargin(Cut: 2.0f, pOtherRect: &ExtraEditor);
3919 ExtraEditor.VSplitRight(Cut: 2.0f, pLeft: &ExtraEditor, pRight: nullptr);
3920 }
3921
3922 static bool s_ShowServerSettingsEditorLast = false;
3923 if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
3924 {
3925 m_EnvelopeEditor.Render(View: ExtraEditor);
3926 }
3927 else if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
3928 {
3929 RenderServerSettingsEditor(View: ExtraEditor, ShowServerSettingsEditorLast: s_ShowServerSettingsEditorLast);
3930 }
3931 else if(m_ActiveExtraEditor == EXTRAEDITOR_HISTORY)
3932 {
3933 RenderEditorHistory(View: ExtraEditor);
3934 }
3935 s_ShowServerSettingsEditorLast = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS;
3936 }
3937 RenderStatusbar(View: StatusBar, pTooltipRect: &TooltipRect);
3938 }
3939
3940 RenderPressedKeys(View);
3941 RenderSavingIndicator(View);
3942
3943 if(m_Dialog == DIALOG_MAPSETTINGS_ERROR)
3944 {
3945 static int s_NullUiTarget = 0;
3946 Ui()->SetHotItem(&s_NullUiTarget);
3947 RenderMapSettingsErrorDialog();
3948 }
3949
3950 if(m_PopupEventActivated)
3951 {
3952 static SPopupMenuId s_PopupEventId;
3953 constexpr float PopupWidth = 400.0f;
3954 constexpr float PopupHeight = 150.0f;
3955 Ui()->DoPopupMenu(pId: &s_PopupEventId, X: Width / 2.0f - PopupWidth / 2.0f, Y: Height / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight, pContext: this, pfnFunc: PopupEvent);
3956 m_PopupEventActivated = false;
3957 m_PopupEventWasActivated = true;
3958 }
3959
3960 if(m_Dialog == DIALOG_NONE && !Ui()->IsPopupHovered() && Ui()->MouseInside(pRect: &View))
3961 {
3962 // handle zoom hotkeys
3963 if(CLineInput::GetActiveInput() == nullptr)
3964 {
3965 // one keypress should be equivalent to zooming
3966 // three times using mousewheel: 1.1^3 = 1.331
3967 if(Input()->KeyPress(Key: KEY_KP_MINUS))
3968 MapView()->Zoom()->ScaleValue(Factor: 1.331f);
3969 if(Input()->KeyPress(Key: KEY_KP_PLUS))
3970 MapView()->Zoom()->ScaleValue(Factor: 1.0f / 1.331f);
3971 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY))
3972 MapView()->ResetZoom();
3973 }
3974
3975 if(m_pBrush->IsEmpty() || !Input()->ShiftIsPressed())
3976 {
3977 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
3978 MapView()->Zoom()->ScaleValue(Factor: 1.1f);
3979 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
3980 MapView()->Zoom()->ScaleValue(Factor: 1.0f / 1.1f);
3981 }
3982 if(!m_pBrush->IsEmpty())
3983 {
3984 bool HasTileAdjustLayer = false;
3985 bool HasSpeedupLayer = false;
3986 for(const auto &pLayer : m_pBrush->m_vpLayers)
3987 {
3988 if(pLayer->m_Type != LAYERTYPE_TILES)
3989 {
3990 continue;
3991 }
3992 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
3993 HasTileAdjustLayer |= pTiles->m_HasTele || pTiles->m_HasSwitch || pTiles->m_HasTune;
3994 HasSpeedupLayer |= pTiles->m_HasSpeedup;
3995 }
3996 if(HasTileAdjustLayer)
3997 {
3998 str_copy(dst&: m_aTooltip, src: "Use Shift+Mouse wheel up/down to adjust the tile numbers. Use Ctrl+F to change all tile numbers to the first unused number.");
3999 }
4000 else if(HasSpeedupLayer)
4001 {
4002 str_copy(dst&: m_aTooltip, src: "Use Shift+Mouse wheel up/down to adjust the angle.");
4003 }
4004
4005 if(Input()->ShiftIsPressed())
4006 {
4007 const int AdjustModifiers = Input()->ModifierIsPressed() ? (Input()->AltIsPressed() ? 2 : 1) : 0;
4008 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
4009 AdjustBrushSpecialTiles(UseNextFree: false, AdjustModifiers, AdjustValue: -1);
4010 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
4011 AdjustBrushSpecialTiles(UseNextFree: false, AdjustModifiers, AdjustValue: 1);
4012 }
4013
4014 // Use ctrl+f to replace number in brush with next free
4015 if(Input()->ModifierIsPressed() && Input()->KeyPress(Key: KEY_F))
4016 AdjustBrushSpecialTiles(UseNextFree: true, AdjustModifiers: 0, AdjustValue: 0);
4017 }
4018 }
4019
4020 m_FileBrowser.Render();
4021 m_Prompt.Render();
4022 m_FontTyper.Render();
4023
4024 MapView()->UpdateZoom();
4025
4026 // Cancel color pipette with escape before closing popup menus with escape
4027 if(m_ColorPipetteActive && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
4028 {
4029 m_ColorPipetteActive = false;
4030 }
4031
4032 Ui()->RenderPopupMenus();
4033 FreeDynamicPopupMenus();
4034
4035 UpdateColorPipette();
4036
4037 if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
4038 {
4039 OnClose();
4040 g_Config.m_ClEditor = 0;
4041 }
4042
4043 // The tooltip can be set in popup menus so we have to render the tooltip after the popup menus.
4044 if(m_GuiActive)
4045 RenderTooltip(TooltipRect);
4046
4047 Ui()->RenderBackButton();
4048 RenderMousePointer();
4049}
4050
4051void CEditor::UpdateBrushPicker()
4052{
4053 if(!m_QuickActionBrushPicker.Disabled() &&
4054 m_Dialog == DIALOG_NONE &&
4055 CLineInput::GetActiveInput() == nullptr)
4056 {
4057 if(Input()->ModifierIsPressed())
4058 {
4059 if(Input()->KeyPress(Key: KEY_SPACE))
4060 {
4061 m_ShowPickerToggle = !m_ShowPickerToggle;
4062 }
4063 m_ShowPicker = m_ShowPickerToggle;
4064 }
4065 else
4066 {
4067 const bool SpacePressed = Input()->KeyIsPressed(Key: KEY_SPACE);
4068 m_ShowPicker = m_ShowPickerToggle || SpacePressed;
4069 if(SpacePressed)
4070 {
4071 m_ShowPickerToggle = false;
4072 }
4073 }
4074 }
4075 else
4076 {
4077 m_ShowPicker = false;
4078 m_ShowPickerToggle = false;
4079 }
4080}
4081
4082void CEditor::RenderPressedKeys(CUIRect View)
4083{
4084 if(!g_Config.m_EdShowkeys)
4085 return;
4086
4087 Ui()->MapScreen();
4088 CTextCursor Cursor;
4089 Cursor.SetPosition(vec2(View.x + 10, View.y + View.h - 24 - 10));
4090 Cursor.m_FontSize = 24.0f;
4091
4092 int NKeys = 0;
4093 for(int i = 0; i < KEY_LAST; i++)
4094 {
4095 if(Input()->KeyIsPressed(Key: i))
4096 {
4097 if(NKeys)
4098 TextRender()->TextEx(pCursor: &Cursor, pText: " + ", Length: -1);
4099 TextRender()->TextEx(pCursor: &Cursor, pText: Input()->KeyName(Key: i), Length: -1);
4100 NKeys++;
4101 }
4102 }
4103}
4104
4105void CEditor::RenderSavingIndicator(CUIRect View)
4106{
4107 if(m_WriterFinishJobs.empty())
4108 return;
4109
4110 const char *pText = "Saving…";
4111 const float FontSize = 24.0f;
4112
4113 Ui()->MapScreen();
4114 CUIRect Label, Spinner;
4115 View.Margin(Cut: 20.0f, pOtherRect: &View);
4116 View.HSplitBottom(Cut: FontSize, pTop: nullptr, pBottom: &View);
4117 View.VSplitRight(Cut: TextRender()->TextWidth(Size: FontSize, pText) + 2.0f, pLeft: &Spinner, pRight: &Label);
4118 Spinner.VSplitRight(Cut: Spinner.h, pLeft: nullptr, pRight: &Spinner);
4119 Ui()->DoLabel(pRect: &Label, pText, Size: FontSize, Align: TEXTALIGN_MR);
4120 Ui()->RenderProgressSpinner(Center: Spinner.Center(), OuterRadius: 8.0f);
4121}
4122
4123void CEditor::FreeDynamicPopupMenus()
4124{
4125 auto Iterator = m_PopupMessageContexts.begin();
4126 while(Iterator != m_PopupMessageContexts.end())
4127 {
4128 if(!Ui()->IsPopupOpen(pId: Iterator->second))
4129 {
4130 CUi::SMessagePopupContext *pContext = Iterator->second;
4131 Iterator = m_PopupMessageContexts.erase(position: Iterator);
4132 delete pContext;
4133 }
4134 else
4135 ++Iterator;
4136 }
4137}
4138
4139void CEditor::UpdateColorPipette()
4140{
4141 if(!m_ColorPipetteActive)
4142 return;
4143
4144 static char s_PipetteScreenButton;
4145 if(Ui()->HotItem() == &s_PipetteScreenButton)
4146 {
4147 // Read color one pixel to the top and left as we would otherwise not read the correct
4148 // color due to the cursor sprite being rendered over the current mouse position.
4149 const int PixelX = std::clamp<int>(val: round_to_int(f: (Ui()->MouseX() - 1.0f) / Ui()->Screen()->w * Graphics()->ScreenWidth()), lo: 0, hi: Graphics()->ScreenWidth() - 1);
4150 const int PixelY = std::clamp<int>(val: round_to_int(f: (Ui()->MouseY() - 1.0f) / Ui()->Screen()->h * Graphics()->ScreenHeight()), lo: 0, hi: Graphics()->ScreenHeight() - 1);
4151 Graphics()->ReadPixel(Position: ivec2(PixelX, PixelY), pColor: &m_PipetteColor);
4152 }
4153
4154 // Simulate button overlaying the entire screen to intercept all clicks for color pipette.
4155 const int ButtonResult = DoButtonLogic(pId: &s_PipetteScreenButton, Checked: 0, pRect: Ui()->Screen(), Flags: BUTTONFLAG_ALL, pToolTip: "Left click to pick a color from the screen. Right click to cancel pipette mode.");
4156 // Don't handle clicks if we are panning, so the pipette stays active while panning.
4157 // Checking m_pContainerPanned alone is not enough, as this variable is reset when
4158 // panning ends before this function is called.
4159 if(m_pContainerPanned == nullptr && m_pContainerPannedLast == nullptr)
4160 {
4161 if(ButtonResult == 1)
4162 {
4163 char aClipboard[9];
4164 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", m_PipetteColor.PackAlphaLast());
4165 Input()->SetClipboardText(aClipboard);
4166
4167 // Check if any of the saved colors is equal to the picked color and
4168 // bring it to the front of the list instead of adding a duplicate.
4169 int ShiftEnd = (int)std::size(m_aSavedColors) - 1;
4170 for(int i = 0; i < (int)std::size(m_aSavedColors); ++i)
4171 {
4172 if(m_aSavedColors[i].Pack() == m_PipetteColor.Pack())
4173 {
4174 ShiftEnd = i;
4175 break;
4176 }
4177 }
4178 for(int i = ShiftEnd; i > 0; --i)
4179 {
4180 m_aSavedColors[i] = m_aSavedColors[i - 1];
4181 }
4182 m_aSavedColors[0] = m_PipetteColor;
4183 }
4184 if(ButtonResult > 0)
4185 {
4186 m_ColorPipetteActive = false;
4187 }
4188 }
4189}
4190
4191void CEditor::RenderMousePointer()
4192{
4193 if(!m_ShowMousePointer)
4194 return;
4195
4196 if(m_MouseAxisLockState == EAxisLock::HORIZONTAL)
4197 {
4198 m_CursorType = CURSOR_RESIZE_H;
4199 }
4200 else if(m_MouseAxisLockState == EAxisLock::VERTICAL)
4201 {
4202 m_CursorType = CURSOR_RESIZE_V;
4203 }
4204
4205 constexpr float CursorSize = 16.0f;
4206
4207 // Cursor
4208 Graphics()->WrapClamp();
4209 Graphics()->TextureSet(Texture: m_aCursorTextures[m_CursorType]);
4210 Graphics()->QuadsBegin();
4211 if(m_CursorType == CURSOR_RESIZE_V)
4212 {
4213 Graphics()->QuadsSetRotation(Angle: pi / 2.0f);
4214 }
4215 if(m_pUiGotContext == Ui()->HotItem())
4216 {
4217 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
4218 }
4219 const float CursorOffset = m_CursorType == CURSOR_RESIZE_V || m_CursorType == CURSOR_RESIZE_H ? -CursorSize / 2.0f : 0.0f;
4220 IGraphics::CQuadItem QuadItem(Ui()->MouseX() + CursorOffset, Ui()->MouseY() + CursorOffset, CursorSize, CursorSize);
4221 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
4222 Graphics()->QuadsEnd();
4223 Graphics()->WrapNormal();
4224
4225 // Pipette color
4226 if(m_ColorPipetteActive)
4227 {
4228 CUIRect PipetteRect = {.x: Ui()->MouseX() + CursorSize, .y: Ui()->MouseY() + CursorSize, .w: 80.0f, .h: 20.0f};
4229 if(PipetteRect.x + PipetteRect.w + 2.0f > Ui()->Screen()->w)
4230 {
4231 PipetteRect.x = Ui()->MouseX() - PipetteRect.w - CursorSize / 2.0f;
4232 }
4233 if(PipetteRect.y + PipetteRect.h + 2.0f > Ui()->Screen()->h)
4234 {
4235 PipetteRect.y = Ui()->MouseY() - PipetteRect.h - CursorSize / 2.0f;
4236 }
4237 PipetteRect.Draw(Color: ColorRGBA(0.2f, 0.2f, 0.2f, 0.7f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
4238
4239 CUIRect Pipette, Label;
4240 PipetteRect.VSplitLeft(Cut: PipetteRect.h, pLeft: &Pipette, pRight: &Label);
4241 Pipette.Margin(Cut: 2.0f, pOtherRect: &Pipette);
4242 Pipette.Draw(Color: m_PipetteColor, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
4243
4244 char aLabel[8];
4245 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "#%06X", m_PipetteColor.PackAlphaLast(Alpha: false));
4246 Ui()->DoLabel(pRect: &Label, pText: aLabel, Size: 10.0f, Align: TEXTALIGN_MC);
4247 }
4248}
4249
4250void CEditor::RenderIngameEntities(const CLayerGroup &Group, const CLayerTiles &TilesLayer)
4251{
4252 const CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
4253 const float TileSize = 32.f;
4254
4255 const bool DDNetOrCustomEntities = std::find_if(first: std::begin(arr: gs_apModEntitiesNames), last: std::end(arr: gs_apModEntitiesNames),
4256 pred: [&](const char *pEntitiesName) { return str_comp_nocase(a: m_SelectEntitiesImage.c_str(), b: pEntitiesName) == 0 &&
4257 str_comp_nocase(a: pEntitiesName, b: "ddnet") != 0; }) == std::end(arr: gs_apModEntitiesNames);
4258
4259 const bool IsSwitch = TilesLayer.m_HasSwitch;
4260 std::function<std::tuple<unsigned char, unsigned char>(int, int)> GetTile;
4261 std::function<unsigned char(int, int)> GetIndexChecked;
4262 if(IsSwitch)
4263 {
4264 const CLayerSwitch &SwitchLayer = static_cast<const CLayerSwitch &>(TilesLayer);
4265 GetTile = [&](int x, int y) -> std::tuple<unsigned char, unsigned char> {
4266 const CSwitchTile Tile = SwitchLayer.m_pSwitchTile[y * SwitchLayer.m_Width + x];
4267 return {Tile.m_Type - ENTITY_OFFSET, Tile.m_Flags};
4268 };
4269 GetIndexChecked = [&](int x, int y) -> unsigned char {
4270 if(x < 0 || y < 0 || x >= SwitchLayer.m_Width || y >= SwitchLayer.m_Height)
4271 {
4272 return 0;
4273 }
4274 return SwitchLayer.m_pSwitchTile[y * SwitchLayer.m_Width + x].m_Type - ENTITY_OFFSET;
4275 };
4276 }
4277 else
4278 {
4279 GetTile = [&](int x, int y) -> std::tuple<unsigned char, unsigned char> {
4280 const CTile Tile = TilesLayer.m_pTiles[y * TilesLayer.m_Width + x];
4281 return {Tile.m_Index - ENTITY_OFFSET, Tile.m_Flags};
4282 };
4283 GetIndexChecked = [&](int x, int y) -> unsigned char {
4284 if(x < 0 || y < 0 || x >= TilesLayer.m_Width || y >= TilesLayer.m_Height)
4285 {
4286 return 0;
4287 }
4288 return TilesLayer.m_pTiles[y * TilesLayer.m_Width + x].m_Index - ENTITY_OFFSET;
4289 };
4290 }
4291
4292 static const ivec2 DOOR_OFFSETS[] = {{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
4293 const ColorRGBA DoorOuterColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClLaserDoorOutlineColor));
4294 const ColorRGBA DoorInnerColor = color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClLaserDoorInnerColor));
4295
4296 CScreenRect ScreenRect = Group.Mapping();
4297 const int ExtraBorder = 9; // doors extend beyond the tile on which they are placed
4298 const int StartX = std::max(a: 0, b: (int)std::floor(x: ScreenRect.m_TopLeft.x / TileSize) - ExtraBorder);
4299 const int EndX = std::min(a: TilesLayer.m_Width, b: (int)std::ceil(x: ScreenRect.m_BottomRight.x / TileSize) + ExtraBorder);
4300 const int StartY = std::max(a: 0, b: (int)std::floor(x: ScreenRect.m_TopLeft.y / TileSize) - ExtraBorder);
4301 const int EndY = std::min(a: TilesLayer.m_Height, b: (int)std::ceil(x: ScreenRect.m_BottomRight.y / TileSize) + ExtraBorder);
4302 for(int y = StartY; y < EndY; y++)
4303 {
4304 for(int x = StartX; x < EndX; x++)
4305 {
4306 const auto [Index, Flags] = GetTile(x, y);
4307
4308 if(Index == ENTITY_DOOR)
4309 {
4310 for(const ivec2 Offset : DOOR_OFFSETS)
4311 {
4312 const unsigned char IndexDoorLength = GetIndexChecked(x + Offset.x, y + Offset.y);
4313 if(IndexDoorLength >= ENTITY_LASER_SHORT && IndexDoorLength <= ENTITY_LASER_LONG)
4314 {
4315 const int Length = (IndexDoorLength - ENTITY_LASER_SHORT + 1) * 3;
4316 const vec2 Pos = vec2(x + 0.5f, y + 0.5f);
4317 const vec2 To = Pos + normalize(v: vec2(Offset.x, Offset.y)) * Length;
4318 pGameClient->m_Items.RenderLaser(From: To * TileSize, Pos: Pos * TileSize, OuterColor: DoorOuterColor, InnerColor: DoorInnerColor, TicksBody: 0.0f, TicksHead: 0.0f, Type: LASERTYPE_DOOR);
4319 }
4320 }
4321 }
4322 else if((!IsSwitch && Index >= ENTITY_FLAGSTAND_RED && Index <= ENTITY_FLAGSTAND_BLUE) ||
4323 (Index >= ENTITY_ARMOR_1 && Index <= ENTITY_WEAPON_LASER) ||
4324 (DDNetOrCustomEntities && Index >= ENTITY_ARMOR_SHOTGUN && Index <= ENTITY_ARMOR_LASER))
4325 {
4326 vec2 Pos = vec2(x, y) * TileSize;
4327 vec2 Scale;
4328 int VisualSize;
4329
4330 if(Index == ENTITY_FLAGSTAND_RED)
4331 {
4332 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpriteFlagRed);
4333 Scale = vec2(42, 84);
4334 VisualSize = 1;
4335 Pos.y -= (Scale.y / 2.f) * 0.75f;
4336 }
4337 else if(Index == ENTITY_FLAGSTAND_BLUE)
4338 {
4339 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpriteFlagBlue);
4340 Scale = vec2(42, 84);
4341 VisualSize = 1;
4342 Pos.y -= (Scale.y / 2.f) * 0.75f;
4343 }
4344 else if(Index == ENTITY_ARMOR_1)
4345 {
4346 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmor);
4347 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_HEALTH, ScaleX&: Scale.x, ScaleY&: Scale.y);
4348 VisualSize = 64;
4349 }
4350 else if(Index == ENTITY_HEALTH_1)
4351 {
4352 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupHealth);
4353 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_HEALTH, ScaleX&: Scale.x, ScaleY&: Scale.y);
4354 VisualSize = 64;
4355 }
4356 else if(Index == ENTITY_WEAPON_SHOTGUN)
4357 {
4358 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_SHOTGUN]);
4359 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_SHOTGUN, ScaleX&: Scale.x, ScaleY&: Scale.y);
4360 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_SHOTGUN].m_VisualSize;
4361 }
4362 else if(Index == ENTITY_WEAPON_GRENADE)
4363 {
4364 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_GRENADE]);
4365 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_GRENADE, ScaleX&: Scale.x, ScaleY&: Scale.y);
4366 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_GRENADE].m_VisualSize;
4367 }
4368 else if(Index == ENTITY_POWERUP_NINJA)
4369 {
4370 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_NINJA]);
4371 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_NINJA, ScaleX&: Scale.x, ScaleY&: Scale.y);
4372 VisualSize = 128;
4373 }
4374 else if(Index == ENTITY_WEAPON_LASER)
4375 {
4376 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_aSpritePickupWeapons[WEAPON_LASER]);
4377 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_LASER, ScaleX&: Scale.x, ScaleY&: Scale.y);
4378 VisualSize = g_pData->m_Weapons.m_aId[WEAPON_LASER].m_VisualSize;
4379 }
4380 else if(Index == ENTITY_ARMOR_SHOTGUN)
4381 {
4382 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorShotgun);
4383 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_SHOTGUN, ScaleX&: Scale.x, ScaleY&: Scale.y);
4384 VisualSize = 64;
4385 }
4386 else if(Index == ENTITY_ARMOR_GRENADE)
4387 {
4388 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorGrenade);
4389 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_GRENADE, ScaleX&: Scale.x, ScaleY&: Scale.y);
4390 VisualSize = 64;
4391 }
4392 else if(Index == ENTITY_ARMOR_NINJA)
4393 {
4394 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorNinja);
4395 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_NINJA, ScaleX&: Scale.x, ScaleY&: Scale.y);
4396 VisualSize = 64;
4397 }
4398 else if(Index == ENTITY_ARMOR_LASER)
4399 {
4400 Graphics()->TextureSet(Texture: pGameClient->m_GameSkin.m_SpritePickupArmorLaser);
4401 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_LASER, ScaleX&: Scale.x, ScaleY&: Scale.y);
4402 VisualSize = 64;
4403 }
4404 else
4405 {
4406 dbg_assert_failed("Unhandled ingame entities index: %d", Index);
4407 }
4408
4409 Graphics()->QuadsBegin();
4410
4411 if(Index != ENTITY_FLAGSTAND_RED &&
4412 Index != ENTITY_FLAGSTAND_BLUE)
4413 {
4414 if(Flags & TILEFLAG_XFLIP)
4415 {
4416 Scale.x = -Scale.x;
4417 }
4418
4419 if(Flags & TILEFLAG_YFLIP)
4420 {
4421 Scale.y = -Scale.y;
4422 }
4423
4424 if(Flags & TILEFLAG_ROTATE)
4425 {
4426 Graphics()->QuadsSetRotation(Angle: 90.f * (pi / 180));
4427
4428 if(Index == ENTITY_POWERUP_NINJA)
4429 {
4430 Pos.y += (Flags & TILEFLAG_XFLIP) ? 10.0f : -10.0f;
4431 }
4432 }
4433 else
4434 {
4435 if(Index == ENTITY_POWERUP_NINJA)
4436 {
4437 Pos.x += (Flags & TILEFLAG_XFLIP) ? 10.0f : -10.0f;
4438 }
4439 }
4440 }
4441
4442 Scale *= VisualSize;
4443 Pos -= (Scale - vec2(TileSize, TileSize)) / 2.0f;
4444 Pos += direction(angle: Client()->GlobalTime() * 2.0f + x + y) * 2.5f;
4445
4446 IGraphics::CQuadItem Quad(Pos.x, Pos.y, Scale.x, Scale.y);
4447 Graphics()->QuadsDrawTL(pArray: &Quad, Num: 1);
4448 Graphics()->QuadsEnd();
4449 }
4450 }
4451 }
4452}
4453
4454void CEditor::Reset()
4455{
4456 Ui()->ClosePopupMenus();
4457 Ui()->SetActiveItem(nullptr);
4458
4459 for(CEditorComponent &Component : m_vComponents)
4460 Component.OnReset();
4461
4462 m_ToolbarPreviewSound = -1;
4463
4464 m_pContainerPanned = nullptr;
4465 m_pContainerPannedLast = nullptr;
4466
4467 m_ActiveEnvelopePreview = EEnvelopePreview::NONE;
4468 m_QuadEnvelopePointOperation = EQuadEnvelopePointOperation::NONE;
4469
4470 m_RenderLayersState.Reset();
4471}
4472
4473void CEditor::AddDefaultMap()
4474{
4475 std::unique_ptr<CEditorMap> pNewMap = std::make_unique<CEditorMap>(args: this);
4476 pNewMap->CreateDefault();
4477 m_vpMaps.push_back(x: std::move(pNewMap));
4478 m_SelectedMap = m_vpMaps.size() - 1;
4479 m_MapTabsRevealSelected = true;
4480 UpdateMapDisplayNames();
4481}
4482
4483void CEditor::CloseMap(size_t Index, bool Confirm)
4484{
4485 if(IsSaving(pFilename: m_vpMaps[Index]->m_aFilename))
4486 {
4487 return;
4488 }
4489
4490 if(Confirm && m_vpMaps[Index]->m_Modified)
4491 {
4492 if(Index != m_SelectedMap)
4493 {
4494 m_SelectedMap = Index;
4495 m_MapTabsRevealSelected = true;
4496 Reset();
4497 }
4498 m_PopupEventType = CEditor::POPEVENT_CLOSE_MAP;
4499 m_PopupEventActivated = true;
4500 return;
4501 }
4502
4503 if(Index == m_SelectedMap)
4504 {
4505 Reset();
4506 }
4507
4508 m_vpMaps.erase(position: m_vpMaps.begin() + Index);
4509 if(m_vpMaps.empty())
4510 {
4511 AddDefaultMap();
4512 }
4513 else
4514 {
4515 UpdateMapDisplayNames();
4516 if(m_SelectedMap > 0 && m_SelectedMap >= Index)
4517 {
4518 --m_SelectedMap;
4519 }
4520 }
4521}
4522
4523IGraphics::CTextureHandle CEditor::GetFrontTexture()
4524{
4525 if(!m_FrontTexture.IsValid())
4526 m_FrontTexture = Graphics()->LoadTexture(pFilename: "editor/front.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4527 return m_FrontTexture;
4528}
4529
4530IGraphics::CTextureHandle CEditor::GetTeleTexture()
4531{
4532 if(!m_TeleTexture.IsValid())
4533 m_TeleTexture = Graphics()->LoadTexture(pFilename: "editor/tele.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4534 return m_TeleTexture;
4535}
4536
4537IGraphics::CTextureHandle CEditor::GetSpeedupTexture()
4538{
4539 if(!m_SpeedupTexture.IsValid())
4540 m_SpeedupTexture = Graphics()->LoadTexture(pFilename: "editor/speedup.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4541 return m_SpeedupTexture;
4542}
4543
4544IGraphics::CTextureHandle CEditor::GetSwitchTexture()
4545{
4546 if(!m_SwitchTexture.IsValid())
4547 m_SwitchTexture = Graphics()->LoadTexture(pFilename: "editor/switch.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4548 return m_SwitchTexture;
4549}
4550
4551IGraphics::CTextureHandle CEditor::GetTuneTexture()
4552{
4553 if(!m_TuneTexture.IsValid())
4554 m_TuneTexture = Graphics()->LoadTexture(pFilename: "editor/tune.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4555 return m_TuneTexture;
4556}
4557
4558IGraphics::CTextureHandle CEditor::GetEntitiesTexture()
4559{
4560 if(!m_EntitiesTexture.IsValid())
4561 m_EntitiesTexture = Graphics()->LoadTexture(pFilename: "editor/entities/DDNet.png", StorageType: IStorage::TYPE_ALL, Flags: Graphics()->TextureLoadFlags());
4562 return m_EntitiesTexture;
4563}
4564
4565void CEditor::Init()
4566{
4567 m_pInput = Kernel()->RequestInterface<IInput>();
4568 m_pClient = Kernel()->RequestInterface<IClient>();
4569 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
4570 m_pConfig = m_pConfigManager->Values();
4571 m_pEngine = Kernel()->RequestInterface<IEngine>();
4572 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
4573 m_pTextRender = Kernel()->RequestInterface<ITextRender>();
4574 m_pStorage = Kernel()->RequestInterface<IStorage>();
4575 m_pSound = Kernel()->RequestInterface<ISound>();
4576 m_UI.Init(pKernel: Kernel());
4577 m_UI.SetPopupMenuClosedCallback([this]() {
4578 m_PopupEventWasActivated = false;
4579 });
4580 m_UI.SetDispatchInputCallback([this](const IInput::CEvent &Event) {
4581 OnInput(Event);
4582 });
4583 m_RenderMap.Init(pGraphics: m_pGraphics, pTextRender: m_pTextRender);
4584
4585 Reset();
4586 AddDefaultMap();
4587
4588 m_vComponents.emplace_back(args&: m_MapView);
4589 m_vComponents.emplace_back(args&: m_EnvelopeEditor);
4590 m_vComponents.emplace_back(args&: m_MapSettingsBackend);
4591 m_vComponents.emplace_back(args&: m_LayerSelector);
4592 m_vComponents.emplace_back(args&: m_FileBrowser);
4593 m_vComponents.emplace_back(args&: m_Prompt);
4594 m_vComponents.emplace_back(args&: m_FontTyper);
4595 m_vComponents.emplace_back(args&: m_QuadKnife);
4596 for(CEditorComponent &Component : m_vComponents)
4597 Component.OnInit(pEditor: this);
4598
4599 m_CheckerTexture = Graphics()->LoadTexture(pFilename: "editor/checker.png", StorageType: IStorage::TYPE_ALL);
4600 m_aCursorTextures[CURSOR_NORMAL] = Graphics()->LoadTexture(pFilename: "editor/cursor.png", StorageType: IStorage::TYPE_ALL);
4601 m_aCursorTextures[CURSOR_RESIZE_H] = Graphics()->LoadTexture(pFilename: "editor/cursor_resize.png", StorageType: IStorage::TYPE_ALL);
4602 m_aCursorTextures[CURSOR_RESIZE_V] = m_aCursorTextures[CURSOR_RESIZE_H];
4603
4604 m_pToolsMap = std::make_unique<CEditorMap>(args: this);
4605
4606 m_pTilesetPicker = std::make_shared<CLayerTiles>(args: m_pToolsMap.get(), args: 16, args: 16);
4607 m_pTilesetPicker->MakePalette();
4608 m_pTilesetPicker->m_Readonly = true;
4609 m_pTilesetPicker->m_RenderOverlays = false;
4610
4611 m_pQuadsetPicker = std::make_shared<CLayerQuads>(args: m_pToolsMap.get());
4612 m_pQuadsetPicker->NewQuad(x: 0, y: 0, Width: 64, Height: 64);
4613 m_pQuadsetPicker->m_Readonly = true;
4614
4615 m_pBrush = std::make_shared<CLayerGroup>(args: m_pToolsMap.get());
4616}
4617
4618bool CEditor::HasUnsavedData() const
4619{
4620 return std::any_of(first: m_vpMaps.begin(), last: m_vpMaps.end(), pred: [](const auto &pMap) { return pMap->m_Modified; });
4621}
4622
4623void CEditor::MouseAxisLock(vec2 &CursorRel)
4624{
4625 if(Input()->AltIsPressed())
4626 {
4627 // only lock with the paint brush and inside editor map area to avoid duplicate Alt behavior
4628 if(m_pBrush->IsEmpty() || Ui()->HotItem() != MapView())
4629 return;
4630
4631 const vec2 CurrentWorldPos = MapView()->MouseWorldPos() / 32.0f;
4632
4633 if(m_MouseAxisLockState == EAxisLock::START)
4634 {
4635 m_MouseAxisInitialPos = CurrentWorldPos;
4636 m_MouseAxisLockState = EAxisLock::NONE;
4637 return; // delta would be 0, calculate it in next frame
4638 }
4639
4640 const vec2 Delta = CurrentWorldPos - m_MouseAxisInitialPos;
4641
4642 // lock to axis if moved mouse by 1 block
4643 if(m_MouseAxisLockState == EAxisLock::NONE && (std::abs(x: Delta.x) > 1.0f || std::abs(x: Delta.y) > 1.0f))
4644 {
4645 m_MouseAxisLockState = (std::abs(x: Delta.x) > std::abs(x: Delta.y)) ? EAxisLock::HORIZONTAL : EAxisLock::VERTICAL;
4646 }
4647
4648 if(m_MouseAxisLockState == EAxisLock::HORIZONTAL)
4649 {
4650 CursorRel.y = 0;
4651 }
4652 else if(m_MouseAxisLockState == EAxisLock::VERTICAL)
4653 {
4654 CursorRel.x = 0;
4655 }
4656 }
4657 else
4658 {
4659 m_MouseAxisLockState = EAxisLock::START;
4660 }
4661}
4662
4663void CEditor::HandleAutosave()
4664{
4665 const float Time = Client()->GlobalTime();
4666 const float LastAutosaveUpdateTime = m_LastAutosaveUpdateTime;
4667 m_LastAutosaveUpdateTime = Time;
4668
4669 if(g_Config.m_EdAutosaveInterval == 0)
4670 {
4671 return; // autosave disabled
4672 }
4673
4674 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
4675 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4676 log_error("editor/autosave", "%s", pErrorMessage);
4677 };
4678 for(const auto &pMap : m_vpMaps)
4679 {
4680 if(!pMap->m_ModifiedAuto || pMap->m_LastModifiedTime < 0.0f)
4681 {
4682 continue; // no unsaved changes
4683 }
4684
4685 // Add time to autosave timer if the editor was disabled for more than 10 seconds,
4686 // to prevent autosave from immediately activating when the editor is activated
4687 // after being deactivated for some time.
4688 if(LastAutosaveUpdateTime >= 0.0f && Time - LastAutosaveUpdateTime > 10.0f)
4689 {
4690 pMap->m_LastSaveTime += Time - LastAutosaveUpdateTime;
4691 }
4692
4693 // Check if autosave timer has expired.
4694 if(pMap->m_LastSaveTime >= Time || Time - pMap->m_LastSaveTime < 60 * g_Config.m_EdAutosaveInterval)
4695 {
4696 continue;
4697 }
4698
4699 // Wait for 5 seconds of no modification before saving, to prevent autosave
4700 // from immediately activating when a map is first modified or while user is
4701 // modifying the map, but don't delay the autosave for more than 1 minute.
4702 if(Time - pMap->m_LastModifiedTime < 5.0f && Time - pMap->m_LastSaveTime < 60 * (g_Config.m_EdAutosaveInterval + 1))
4703 {
4704 continue;
4705 }
4706
4707 pMap->PerformAutosave(ErrorHandler);
4708 }
4709}
4710
4711void CEditor::HandleWriterFinishJobs()
4712{
4713 if(m_WriterFinishJobs.empty())
4714 return;
4715
4716 std::shared_ptr<CDataFileWriterFinishJob> pJob = m_WriterFinishJobs.front();
4717 if(!pJob->Done())
4718 return;
4719 m_WriterFinishJobs.pop_front();
4720
4721 const char *pErrorMessage = pJob->ErrorMessage();
4722 if(pErrorMessage[0] != '\0')
4723 {
4724 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4725 return;
4726 }
4727
4728 auto MapIt = std::find_if(first: m_vpMaps.begin(), last: m_vpMaps.end(), pred: [&](const std::unique_ptr<CEditorMap> &pMap) {
4729 return str_comp(a: pMap->m_aFilename, b: pJob->RealFilename()) == 0;
4730 });
4731 if(MapIt != m_vpMaps.end())
4732 {
4733 const auto &pMap = *MapIt;
4734 if(pMap->m_CloseOnSave)
4735 {
4736 dbg_assert(!pMap->m_Modified, "Map to be closed was modified, should be prevented in OnModify");
4737 CloseMap(Index: MapIt - m_vpMaps.begin(), Confirm: false);
4738 }
4739 }
4740
4741 // send rcon.. if we can
4742 if(Client()->RconAuthed() && g_Config.m_EdAutoMapReload)
4743 {
4744 const CServerInfo &CurrentServerInfo = Client()->ServerInfo();
4745 if(net_addr_is_local(addr: &Client()->ServerAddress()))
4746 {
4747 char aMapName[MAX_MAP_LENGTH];
4748 fs_split_file_extension(filename: fs_filename(path: pJob->RealFilename()), name: aMapName, name_size: sizeof(aMapName));
4749 if(!str_comp(a: aMapName, b: CurrentServerInfo.m_aMap))
4750 Client()->Rcon(pLine: "hot_reload");
4751 }
4752 }
4753}
4754
4755bool CEditor::IsSaving(const char *pFilename) const
4756{
4757 return std::any_of(first: std::begin(cont: m_WriterFinishJobs), last: std::end(cont: m_WriterFinishJobs), pred: [&](const std::shared_ptr<CDataFileWriterFinishJob> &Job) {
4758 return str_comp(a: pFilename, b: Job->RealFilename()) == 0;
4759 });
4760}
4761
4762void CEditor::UpdateMapDisplayNames()
4763{
4764 std::unordered_map<std::string, int> Occurrences;
4765 for(const auto &pMap : m_vpMaps)
4766 {
4767 auto It = Occurrences.find(key: pMap->m_aFilename);
4768 if(It == Occurrences.end())
4769 {
4770 Occurrences.emplace(args&: pMap->m_aFilename, args: 1);
4771 }
4772 else
4773 {
4774 It->second++;
4775 }
4776 }
4777 std::unordered_map<std::string, int> Counts;
4778 for(const auto &pMap : m_vpMaps)
4779 {
4780 if(pMap->m_aFilename[0] == '\0')
4781 {
4782 str_copy(dst&: pMap->m_aDisplayName, src: "Unnamed");
4783 str_copy(dst&: pMap->m_aAutosaveName, src: "unnamed");
4784 }
4785 else
4786 {
4787 fs_split_file_extension(filename: fs_filename(path: pMap->m_aFilename), name: pMap->m_aDisplayName, name_size: sizeof(pMap->m_aDisplayName));
4788 str_copy(dst&: pMap->m_aAutosaveName, src: pMap->m_aDisplayName);
4789 }
4790 if(Occurrences[pMap->m_aFilename] <= 1)
4791 {
4792 continue;
4793 }
4794
4795 auto Count = Counts.find(key: pMap->m_aFilename);
4796 if(Count == Counts.end())
4797 {
4798 Count = Counts.emplace(args&: pMap->m_aFilename, args: 1).first;
4799 }
4800 else
4801 {
4802 Count->second++;
4803 }
4804 char aNum[16];
4805 str_format(buffer: aNum, buffer_size: sizeof(aNum), format: " (%d)", Count->second);
4806 str_append(dst&: pMap->m_aDisplayName, src: aNum);
4807 str_format(buffer: aNum, buffer_size: sizeof(aNum), format: "_%02d", Count->second);
4808 str_append(dst&: pMap->m_aAutosaveName, src: aNum);
4809 }
4810}
4811
4812void CEditor::OnUpdate()
4813{
4814 CUIElementBase::Init(pUI: Ui()); // update static pointer because game and editor use separate UI
4815
4816 m_pContainerPannedLast = m_pContainerPanned;
4817
4818 // handle mouse movement
4819 vec2 CursorRel = vec2(0.0f, 0.0f);
4820 IInput::ECursorType CursorType = Input()->CursorRelative(pX: &CursorRel.x, pY: &CursorRel.y);
4821 if(CursorType != IInput::CURSOR_NONE)
4822 {
4823 Ui()->ConvertMouseMove(pX: &CursorRel.x, pY: &CursorRel.y, CursorType);
4824 MouseAxisLock(CursorRel);
4825 Ui()->OnCursorMove(X: CursorRel.x, Y: CursorRel.y);
4826 }
4827
4828 // handle key presses
4829 Input()->ConsumeEvents(Consumer: [&](const IInput::CEvent &Event) {
4830 OnInput(Event);
4831 });
4832
4833 MapView()->UpdateMouseWorld();
4834 LayerSelector()->UpdateHoveredTiles();
4835 HandleAutosave();
4836 HandleWriterFinishJobs();
4837
4838 for(CEditorComponent &Component : m_vComponents)
4839 Component.OnUpdate();
4840}
4841
4842void CEditor::OnInput(const IInput::CEvent &Event)
4843{
4844 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Event.m_Key == KEY_F1)
4845 {
4846 if((Event.m_Flags & IInput::FLAG_PRESS) != 0 && (Event.m_Flags & IInput::FLAG_REPEAT) == 0)
4847 {
4848 m_QuickActionShowHelp.Call();
4849 }
4850 return;
4851 }
4852
4853 for(CEditorComponent &Component : m_vComponents)
4854 {
4855 // Events with flag `FLAG_RELEASE` must always be forwarded to all components so keys being
4856 // released can be handled in all components also after some components have been disabled.
4857 if(Component.OnInput(Event) && (Event.m_Flags & ~IInput::FLAG_RELEASE) != 0)
4858 return;
4859 }
4860 Ui()->OnInput(Event);
4861}
4862
4863void CEditor::OnRender()
4864{
4865 Ui()->SetMouseSlow(false);
4866
4867 // toggle gui
4868 if(m_Dialog == DIALOG_NONE &&
4869 CLineInput::GetActiveInput() == nullptr &&
4870 !Input()->ModifierIsPressed() &&
4871 !Input()->ShiftIsPressed() &&
4872 Input()->KeyPress(Key: KEY_TAB))
4873 {
4874 m_GuiActive = !m_GuiActive;
4875 }
4876
4877 if(Input()->KeyPress(Key: KEY_F10))
4878 m_ShowMousePointer = false;
4879
4880 if(Map()->m_EnvelopeEvaluator.m_Animate)
4881 Map()->m_EnvelopeEvaluator.m_AnimateTime = Client()->GlobalTime() - Map()->m_EnvelopeEvaluator.m_AnimateStart;
4882
4883 m_pUiGotContext = nullptr;
4884 Ui()->StartCheck();
4885
4886 Ui()->Update();
4887
4888 Ui()->DoBackButton();
4889
4890 Render();
4891
4892 MapView()->ResetMouseDeltaWorld();
4893
4894 if(Input()->KeyPress(Key: KEY_F10))
4895 {
4896 Graphics()->TakeScreenshot(pFilename: nullptr);
4897 m_ShowMousePointer = true;
4898 }
4899
4900 if(g_Config.m_Debug)
4901 Ui()->DebugRender(X: 2.0f, Y: Ui()->Screen()->h - 27.0f);
4902
4903 Ui()->FinishCheck();
4904 Ui()->ClearHotkeys();
4905 Input()->Clear();
4906
4907 CLineInput::RenderCandidates();
4908
4909#if defined(CONF_DEBUG)
4910 Map()->CheckIntegrity();
4911#endif
4912}
4913
4914void CEditor::OnActivate()
4915{
4916 ResetMentions();
4917 ResetIngameMoved();
4918}
4919
4920void CEditor::OnWindowResize()
4921{
4922 Ui()->OnWindowResize();
4923}
4924
4925void CEditor::OnClose()
4926{
4927 m_ColorPipetteActive = false;
4928
4929 if(m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
4930 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
4931
4932 m_FileBrowser.OnEditorClose();
4933}
4934
4935void CEditor::OnDialogClose()
4936{
4937 m_Dialog = DIALOG_NONE;
4938 m_FileBrowser.OnDialogClose();
4939}
4940
4941void CEditor::LoadIngameMap()
4942{
4943 CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
4944
4945 if(Load(pFilename: pGameClient->Map()->Path(), StorageType: IStorage::TYPE_SAVE))
4946 {
4947 Map()->m_ValidSaveFilename = !str_startswith(str: pGameClient->Map()->Path(), prefix: "downloadedmaps/");
4948 }
4949 else
4950 {
4951 Load(pFilename: pGameClient->Map()->Path(), StorageType: IStorage::TYPE_ALL);
4952 Map()->m_ValidSaveFilename = false;
4953 }
4954
4955 vec2 Center = pGameClient->m_Camera.m_Center;
4956 MapView()->SetWorldOffset(Center);
4957}
4958
4959bool CEditor::Save(const char *pFilename)
4960{
4961 if(IsSaving(pFilename))
4962 {
4963 return false;
4964 }
4965
4966 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
4967 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4968 log_error("editor/save", "%s", pErrorMessage);
4969 };
4970 return Map()->Save(pFilename, ErrorHandler);
4971}
4972
4973bool CEditor::HandleMapDrop(const char *pFilename, int StorageType)
4974{
4975 OnDialogClose();
4976 return Load(pFilename, StorageType);
4977}
4978
4979bool CEditor::Load(const char *pFilename, int StorageType)
4980{
4981 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
4982 ShowFileDialogError(pFormat: "%s", pErrorMessage);
4983 log_error("editor/load", "%s", pErrorMessage);
4984 };
4985
4986 Reset();
4987 std::unique_ptr<CEditorMap> pNewMap = std::make_unique<CEditorMap>(args: this);
4988 const bool Result = pNewMap->Load(pFilename, StorageType, ErrorHandler: std::move(ErrorHandler));
4989 if(Result)
4990 {
4991 m_vpMaps.push_back(x: std::move(pNewMap));
4992 m_SelectedMap = m_vpMaps.size() - 1;
4993 m_MapTabsRevealSelected = true;
4994 UpdateMapDisplayNames();
4995
4996 for(CEditorComponent &Component : m_vComponents)
4997 Component.OnMapLoad();
4998
4999 log_info("editor/load", "Loaded map '%s'", Map()->m_aFilename);
5000 }
5001 return Result;
5002}
5003
5004CEditorMap *CEditor::Map()
5005{
5006 dbg_assert(m_SelectedMap < m_vpMaps.size(), "Invalid m_SelectedMap: %" PRIzu, m_SelectedMap);
5007 return m_vpMaps[m_SelectedMap].get();
5008}
5009
5010const CEditorMap *CEditor::Map() const
5011{
5012 dbg_assert(m_SelectedMap < m_vpMaps.size(), "Invalid m_SelectedMap: %" PRIzu, m_SelectedMap);
5013 return m_vpMaps[m_SelectedMap].get();
5014}
5015
5016CEditorHistory &CEditor::ActiveHistory()
5017{
5018 if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
5019 {
5020 return Map()->m_ServerSettingsHistory;
5021 }
5022 else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
5023 {
5024 return Map()->m_EnvelopeEditorHistory;
5025 }
5026 else
5027 {
5028 return Map()->m_EditorHistory;
5029 }
5030}
5031
5032void CEditor::AdjustBrushSpecialTiles(bool UseNextFree, int AdjustModifiers, int AdjustValue)
5033{
5034 // Adjust m_Angle of speedup or m_Number field of tune, switch and tele tiles by `Adjust` if `UseNextFree` is false
5035 // If `AdjustValue` is 0 and `UseNextFree` is false, then update numbers of brush tiles to global values
5036 // If true, then use the next free number instead
5037
5038 dbg_assert(AdjustValue == -1 || AdjustValue == 0 || AdjustValue == 1, "Invalid AdjustValue: %d", AdjustValue);
5039 auto &&AdjustNumber = [AdjustValue](auto &Number, int Min, int Max) {
5040 const int NumberInt = Number + AdjustValue; // Cast to int so this does not overflow unsigned char for some tiles
5041 if(NumberInt < Min)
5042 {
5043 Number = Max;
5044 }
5045 else if(NumberInt > Max)
5046 {
5047 Number = Min;
5048 }
5049 else
5050 {
5051 Number = NumberInt;
5052 }
5053 };
5054
5055 for(auto &pLayer : m_pBrush->m_vpLayers)
5056 {
5057 if(pLayer->m_Type != LAYERTYPE_TILES)
5058 continue;
5059
5060 std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
5061
5062 if(pLayerTiles->m_HasTele && (!UseNextFree || Map()->m_pTeleLayer != nullptr))
5063 {
5064 const int NextFreeTeleNumber = UseNextFree ? Map()->m_pTeleLayer->FindNextFreeNumber(Checkpoint: false) : 0;
5065 const int NextFreeCheckpointNumber = UseNextFree ? Map()->m_pTeleLayer->FindNextFreeNumber(Checkpoint: true) : 0;
5066 std::shared_ptr<CLayerTele> pTeleLayer = std::static_pointer_cast<CLayerTele>(r: pLayer);
5067 for(int y = 0; y < pTeleLayer->m_Height; y++)
5068 {
5069 for(int x = 0; x < pTeleLayer->m_Width; x++)
5070 {
5071 int i = y * pTeleLayer->m_Width + x;
5072 if(!IsValidTeleTile(Index: pTeleLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTeleLayer->m_pTeleTile[i].m_Number))
5073 continue;
5074
5075 if(UseNextFree)
5076 {
5077 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
5078 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeCheckpointNumber;
5079 else if(IsTeleTileNumberUsedAny(Index: pTeleLayer->m_pTiles[i].m_Index))
5080 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeTeleNumber;
5081 }
5082 else if(AdjustValue == 0)
5083 {
5084 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
5085 pTeleLayer->m_pTeleTile[i].m_Number = m_TeleCheckpointNumber;
5086 else if(IsTeleTileNumberUsedAny(Index: pTeleLayer->m_pTiles[i].m_Index))
5087 pTeleLayer->m_pTeleTile[i].m_Number = m_TeleNumber;
5088 }
5089 else if(AdjustModifiers == 0)
5090 {
5091 AdjustNumber(pTeleLayer->m_pTeleTile[i].m_Number, 1, 255);
5092 }
5093 }
5094 }
5095 }
5096 else if(pLayerTiles->m_HasTune && (!UseNextFree || Map()->m_pTuneLayer != nullptr))
5097 {
5098 const int NextFreeNumber = UseNextFree ? Map()->m_pTuneLayer->FindNextFreeNumber() : 0;
5099 std::shared_ptr<CLayerTune> pTuneLayer = std::static_pointer_cast<CLayerTune>(r: pLayer);
5100 for(int y = 0; y < pTuneLayer->m_Height; y++)
5101 {
5102 for(int x = 0; x < pTuneLayer->m_Width; x++)
5103 {
5104 int i = y * pTuneLayer->m_Width + x;
5105 if(!IsValidTuneTile(Index: pTuneLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTuneLayer->m_pTuneTile[i].m_Number))
5106 continue;
5107
5108 if(UseNextFree)
5109 {
5110 pTuneLayer->m_pTuneTile[i].m_Number = NextFreeNumber;
5111 }
5112 else if(AdjustModifiers == 0)
5113 {
5114 AdjustNumber(pTuneLayer->m_pTuneTile[i].m_Number, 1, 255);
5115 }
5116 }
5117 }
5118 }
5119 else if(pLayerTiles->m_HasSwitch && (!UseNextFree || Map()->m_pSwitchLayer != nullptr))
5120 {
5121 const int NextFreeNumber = UseNextFree ? Map()->m_pSwitchLayer->FindNextFreeNumber() : 0;
5122 std::shared_ptr<CLayerSwitch> pSwitchLayer = std::static_pointer_cast<CLayerSwitch>(r: pLayer);
5123 for(int y = 0; y < pSwitchLayer->m_Height; y++)
5124 {
5125 for(int x = 0; x < pSwitchLayer->m_Width; x++)
5126 {
5127 int i = y * pSwitchLayer->m_Width + x;
5128 if(!IsValidSwitchTile(Index: pSwitchLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pSwitchLayer->m_pSwitchTile[i].m_Number))
5129 continue;
5130
5131 if(UseNextFree)
5132 {
5133 pSwitchLayer->m_pSwitchTile[i].m_Number = NextFreeNumber;
5134 }
5135 else if(AdjustModifiers == 0)
5136 {
5137 AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Number, 1, 255);
5138 }
5139 else if(AdjustModifiers == 1)
5140 {
5141 AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Delay, 0, 255);
5142 }
5143 }
5144 }
5145 }
5146 else if(pLayerTiles->m_HasSpeedup && !UseNextFree)
5147 {
5148 std::shared_ptr<CLayerSpeedup> pSpeedupLayer = std::static_pointer_cast<CLayerSpeedup>(r: pLayer);
5149 for(int y = 0; y < pSpeedupLayer->m_Height; y++)
5150 {
5151 for(int x = 0; x < pSpeedupLayer->m_Width; x++)
5152 {
5153 int i = y * pSpeedupLayer->m_Width + x;
5154 if(!IsValidSpeedupTile(Index: pSpeedupLayer->m_pTiles[i].m_Index))
5155 continue;
5156
5157 if(AdjustValue == 0)
5158 {
5159 pSpeedupLayer->m_pSpeedupTile[i].m_Angle = m_SpeedupAngle;
5160 pSpeedupLayer->m_SpeedupAngle = m_SpeedupAngle;
5161 }
5162 else if(AdjustModifiers == 0)
5163 {
5164 AdjustNumber(pSpeedupLayer->m_pSpeedupTile[i].m_Angle, 0, 359);
5165 }
5166 else if(AdjustModifiers == 1)
5167 {
5168 AdjustNumber(pSpeedupLayer->m_pSpeedupTile[i].m_Force, 1, 255);
5169 }
5170 else if(AdjustModifiers == 2)
5171 {
5172 AdjustNumber(pSpeedupLayer->m_pSpeedupTile[i].m_MaxSpeed, 0, 255);
5173 }
5174 }
5175 }
5176 }
5177 }
5178}
5179
5180IEditor *CreateEditor() { return new CEditor; }
5181