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