A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEditor.UIElements;
4using UnityEngine;
5using UnityEngine.UIElements;
6
7namespace UnityEditor.Rendering.LookDev
8{
9 /// <summary>Interface that must implement the EnvironmentLibrary view to communicate with the data management</summary>
10 public interface IEnvironmentDisplayer
11 {
12 /// <summary>Repaint the UI</summary>
13 void Repaint();
14
15 /// <summary>Callback on Environment change in the Library</summary>
16 event Action<EnvironmentLibrary> OnChangingEnvironmentLibrary;
17 }
18
19 partial class DisplayWindow : IEnvironmentDisplayer
20 {
21 static partial class Style
22 {
23 internal static readonly Texture2D k_AddIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Add", forceLowRes: true);
24 internal static readonly Texture2D k_RemoveIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Remove", forceLowRes: true);
25 internal static readonly Texture2D k_DuplicateIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Duplicate", forceLowRes: true);
26
27 internal const string k_DragAndDropLibrary = "Drag and drop EnvironmentLibrary here";
28 }
29
30 VisualElement m_EnvironmentContainer;
31 ListView m_EnvironmentList;
32 EnvironmentElement m_EnvironmentInspector;
33 UIElements.Toolbar m_EnvironmentListToolbar;
34 UIElements.ObjectField m_LibraryField;
35
36 //event Action<UnityEngine.Object> OnAddingEnvironmentInternal;
37 //event Action<UnityEngine.Object> IEnvironmentDisplayer.OnAddingEnvironment
38 //{
39 // add => OnAddingEnvironmentInternal += value;
40 // remove => OnAddingEnvironmentInternal -= value;
41 //}
42
43 //event Action<int> OnRemovingEnvironmentInternal;
44 //event Action<int> IEnvironmentDisplayer.OnRemovingEnvironment
45 //{
46 // add => OnRemovingEnvironmentInternal += value;
47 // remove => OnRemovingEnvironmentInternal -= value;
48 //}
49
50 event Action<EnvironmentLibrary> OnChangingEnvironmentLibraryInternal;
51 event Action<EnvironmentLibrary> IEnvironmentDisplayer.OnChangingEnvironmentLibrary
52 {
53 add => OnChangingEnvironmentLibraryInternal += value;
54 remove => OnChangingEnvironmentLibraryInternal -= value;
55 }
56
57 static int FirstVisibleIndex(ListView listView)
58 => (int)(listView.Q<ScrollView>().scrollOffset.y / (int)listView.fixedItemHeight);
59
60 void CreateEnvironment()
61 {
62 if (m_MainContainer == null || m_MainContainer.Equals(null))
63 throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateEnvironment()");
64
65 m_EnvironmentContainer = new VisualElement() { name = Style.k_EnvironmentContainerName };
66 m_MainContainer.Add(m_EnvironmentContainer);
67 if (sidePanel == SidePanel.Environment)
68 m_MainContainer.AddToClassList(Style.k_ShowEnvironmentPanelClass);
69
70 m_EnvironmentInspector = new EnvironmentElement(withPreview: false, () =>
71 {
72 LookDev.SaveContextChangeAndApply(ViewIndex.First);
73 LookDev.SaveContextChangeAndApply(ViewIndex.Second);
74 });
75 m_EnvironmentList = new ListView();
76 m_EnvironmentList.AddToClassList("list-environment");
77 m_EnvironmentList.selectionType = SelectionType.Single;
78 m_EnvironmentList.fixedItemHeight = EnvironmentElement.k_SkyThumbnailHeight;
79 m_EnvironmentList.makeItem = () =>
80 {
81 var preview = new Image();
82 preview.AddManipulator(new EnvironmentPreviewDragger(this, m_ViewContainer));
83 return preview;
84 };
85 m_EnvironmentList.bindItem = (e, i) =>
86 {
87 if (LookDev.currentContext.environmentLibrary == null)
88 return;
89
90 (e as Image).image = EnvironmentElement.GetLatLongThumbnailTexture(
91 LookDev.currentContext.environmentLibrary[i],
92 EnvironmentElement.k_SkyThumbnailWidth);
93 };
94#if UNITY_2022_2_OR_NEWER
95 m_EnvironmentList.selectionChanged += objects =>
96 {
97 bool empty = !objects.GetEnumerator().MoveNext();
98 if (empty || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
99#elif UNITY_2020_1_OR_NEWER
100 m_EnvironmentList.onSelectionChange += objects =>
101 {
102 bool empty = !objects.GetEnumerator().MoveNext();
103 if (empty || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
104#else
105 m_EnvironmentList.onSelectionChanged += objects =>
106
107 {
108 if (objects.Count == 0 || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
109#endif
110 {
111 m_EnvironmentInspector.style.visibility = Visibility.Hidden;
112 m_EnvironmentInspector.style.height = 0;
113 }
114 else
115 {
116 m_EnvironmentInspector.style.visibility = Visibility.Visible;
117 m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
118 int firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
119 Environment environment = LookDev.currentContext.environmentLibrary[m_EnvironmentList.selectedIndex];
120 var container = m_EnvironmentList.Q("unity-content-container");
121 if (m_EnvironmentList.selectedIndex - firstVisibleIndex >= container.childCount || m_EnvironmentList.selectedIndex < firstVisibleIndex)
122 {
123 m_EnvironmentList.ScrollToItem(m_EnvironmentList.selectedIndex);
124 firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
125 }
126 Image deportedLatLong = container[m_EnvironmentList.selectedIndex - firstVisibleIndex] as Image;
127 m_EnvironmentInspector.Bind(environment, deportedLatLong);
128 }
129 };
130#if UNITY_2022_2_OR_NEWER
131 m_EnvironmentList.itemsChosen += objCollection =>
132 {
133 foreach (var obj in objCollection)
134 EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary ? [(int)obj]);
135 };
136#elif UNITY_2020_1_OR_NEWER
137 m_EnvironmentList.onItemsChosen += objCollection =>
138 {
139 foreach (var obj in objCollection)
140 EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary ? [(int)obj]);
141 };
142#else
143 m_EnvironmentList.onItemChosen += obj =>
144 EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary?[(int)obj]);
145#endif
146 m_NoEnvironmentList = new Label(Style.k_DragAndDropLibrary);
147 m_NoEnvironmentList.style.flexGrow = 1;
148 m_NoEnvironmentList.style.unityTextAlign = TextAnchor.MiddleCenter;
149 m_EnvironmentContainer.Add(m_EnvironmentInspector);
150
151 m_EnvironmentListToolbar = new UIElements.Toolbar();
152 ToolbarButton addEnvironment = new ToolbarButton(() =>
153 {
154 if (LookDev.currentContext.environmentLibrary == null)
155 return;
156
157 LookDev.currentContext.environmentLibrary.Add();
158 RefreshLibraryDisplay();
159 m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
160 m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
161 ScrollToEnd();
162 })
163 {
164 name = "add",
165 tooltip = "Add new empty environment"
166 };
167 addEnvironment.Add(new Image() { image = Style.k_AddIcon });
168 ToolbarButton removeEnvironment = new ToolbarButton(() =>
169 {
170 if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
171 return;
172 LookDev.currentContext.environmentLibrary?.Remove(m_EnvironmentList.selectedIndex);
173 RefreshLibraryDisplay();
174 m_EnvironmentList.selectedIndex = -1;
175 })
176 {
177 name = "remove",
178 tooltip = "Remove environment currently selected"
179 };
180 removeEnvironment.Add(new Image() { image = Style.k_RemoveIcon });
181 ToolbarButton duplicateEnvironment = new ToolbarButton(() =>
182 {
183 if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
184 return;
185 LookDev.currentContext.environmentLibrary.Duplicate(m_EnvironmentList.selectedIndex);
186 RefreshLibraryDisplay();
187 m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
188 m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
189 ScrollToEnd();
190 })
191 {
192 name = "duplicate",
193 tooltip = "Duplicate environment currently selected"
194 };
195 duplicateEnvironment.Add(new Image() { image = Style.k_DuplicateIcon });
196 m_EnvironmentListToolbar.Add(addEnvironment);
197 m_EnvironmentListToolbar.Add(removeEnvironment);
198 m_EnvironmentListToolbar.Add(duplicateEnvironment);
199 m_EnvironmentListToolbar.AddToClassList("list-environment-overlay");
200
201 var m_EnvironmentInspectorSeparator = new VisualElement() { name = "separator-line" };
202 m_EnvironmentInspectorSeparator.Add(new VisualElement() { name = "separator" });
203 m_EnvironmentContainer.Add(m_EnvironmentInspectorSeparator);
204
205 VisualElement listContainer = new VisualElement();
206 listContainer.AddToClassList("list-environment");
207 listContainer.Add(m_EnvironmentList);
208 listContainer.Add(m_EnvironmentListToolbar);
209
210 m_LibraryField = new ObjectField("Library")
211 {
212 tooltip = "The currently used library"
213 };
214 m_LibraryField.allowSceneObjects = false;
215 m_LibraryField.objectType = typeof(EnvironmentLibrary);
216 m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
217 m_LibraryField.RegisterValueChangedCallback(evt =>
218 {
219 m_EnvironmentList.selectedIndex = -1;
220 OnChangingEnvironmentLibraryInternal?.Invoke(evt.newValue as EnvironmentLibrary);
221 RefreshLibraryDisplay();
222 });
223
224 var environmentListCreationToolbar = new UIElements.Toolbar()
225 {
226 name = "environmentListCreationToolbar"
227 };
228 environmentListCreationToolbar.Add(m_LibraryField);
229 environmentListCreationToolbar.Add(new ToolbarButton(()
230 => EnvironmentLibraryCreator.CreateAndAssignTo(m_LibraryField))
231 {
232 text = "New",
233 tooltip = "Create a new EnvironmentLibrary"
234 });
235
236 m_EnvironmentContainer.Add(listContainer);
237 m_EnvironmentContainer.Add(m_NoEnvironmentList);
238 m_EnvironmentContainer.Add(environmentListCreationToolbar);
239
240 //add ability to unselect
241 m_EnvironmentList.RegisterCallback<MouseDownEvent>(evt =>
242 {
243 var clickedIndex = (int)(evt.localMousePosition.y / (int)m_EnvironmentList.fixedItemHeight);
244 if (clickedIndex >= m_EnvironmentList.itemsSource.Count)
245 {
246 m_EnvironmentList.selectedIndex = -1;
247 evt.StopPropagation();
248 }
249 });
250
251 RefreshLibraryDisplay();
252 }
253
254 //necessary as the scrollview need to be updated which take some editor frames.
255 void ScrollToEnd(int attemptRemaining = 5)
256 {
257 m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
258 if (attemptRemaining > 0)
259 EditorApplication.delayCall += () => ScrollToEnd(--attemptRemaining);
260 }
261
262 void RefreshLibraryDisplay()
263 {
264 if (m_LibraryField != null)
265 m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
266
267 if (m_EnvironmentInspector != null && m_EnvironmentList != null)
268 {
269 int itemMax = LookDev.currentContext.environmentLibrary?.Count ?? 0;
270 if (itemMax == 0 || m_EnvironmentList.selectedIndex == -1)
271 {
272 m_EnvironmentInspector.style.visibility = Visibility.Hidden;
273 m_EnvironmentInspector.style.height = 0;
274 }
275 else
276 {
277 m_EnvironmentInspector.style.visibility = Visibility.Visible;
278 m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
279 }
280
281 var items = new List<int>(itemMax);
282 for (int i = 0; i < itemMax; i++)
283 items.Add(i);
284 m_EnvironmentList.itemsSource = items;
285 if (LookDev.currentContext.environmentLibrary == null)
286 {
287 m_EnvironmentList
288 .Q(className: "unity-scroll-view__vertical-scroller")
289 .Q("unity-dragger")
290 .style.visibility = Visibility.Hidden;
291 m_EnvironmentListToolbar.style.visibility = Visibility.Hidden;
292 m_NoEnvironmentList.style.display = DisplayStyle.Flex;
293 }
294 else
295 {
296 m_EnvironmentList
297 .Q(className: "unity-scroll-view__vertical-scroller")
298 .Q("unity-dragger")
299 .style.visibility = itemMax == 0
300 ? Visibility.Hidden
301 : Visibility.Visible;
302 m_EnvironmentListToolbar.style.visibility = Visibility.Visible;
303 m_NoEnvironmentList.style.display = DisplayStyle.None;
304 }
305 m_EnvironmentList.RefreshItems();
306 }
307 }
308
309 DraggingContext StartDragging(VisualElement item, Vector2 worldPosition)
310 => new DraggingContext(
311 rootVisualElement,
312 item as Image,
313 //note: this even can come before the selection event of the
314 //ListView. Reconstruct index by looking at target of the event.
315 (int)item.layout.y / (int)m_EnvironmentList.fixedItemHeight,
316 worldPosition);
317
318 void EndDragging(DraggingContext context, Vector2 mouseWorldPosition)
319 {
320 Environment environment = LookDev.currentContext.environmentLibrary?[context.draggedIndex];
321 if (environment == null)
322 return;
323
324 if (m_Views[(int)ViewIndex.First].ContainsPoint(mouseWorldPosition))
325 {
326 if (viewLayout == Layout.CustomSplit)
327 OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Composite, mouseWorldPosition);
328 else
329 OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.First, mouseWorldPosition);
330 m_NoEnvironment1.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
331 }
332 else
333 {
334 OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Second, mouseWorldPosition);
335 m_NoEnvironment2.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
336 }
337 }
338
339 class DraggingContext : IDisposable
340 {
341 const string k_CursorFollowerName = "cursorFollower";
342 public readonly int draggedIndex;
343 readonly Image cursorFollower;
344 readonly Vector2 cursorOffset;
345 readonly VisualElement windowContent;
346
347 public DraggingContext(VisualElement windowContent, Image draggedElement, int draggedIndex, Vector2 worldPosition)
348 {
349 this.windowContent = windowContent;
350 this.draggedIndex = draggedIndex;
351 cursorFollower = new Image()
352 {
353 name = k_CursorFollowerName,
354 image = draggedElement.image
355 };
356 cursorFollower.tintColor = new Color(1f, 1f, 1f, .5f);
357 windowContent.Add(cursorFollower);
358 cursorOffset = draggedElement.WorldToLocal(worldPosition);
359
360 cursorFollower.style.position = Position.Absolute;
361 UpdateCursorFollower(worldPosition);
362 }
363
364 public void UpdateCursorFollower(Vector2 mouseWorldPosition)
365 {
366 Vector2 windowLocalPosition = windowContent.WorldToLocal(mouseWorldPosition);
367 cursorFollower.style.left = windowLocalPosition.x - cursorOffset.x;
368 cursorFollower.style.top = windowLocalPosition.y - cursorOffset.y;
369 }
370
371 public void Dispose()
372 {
373 if (windowContent.Contains(cursorFollower))
374 windowContent.Remove(cursorFollower);
375 }
376 }
377
378 class EnvironmentPreviewDragger : Manipulator
379 {
380 VisualElement m_DropArea;
381 DisplayWindow m_Window;
382
383 //Note: static as only one drag'n'drop at a time
384 static DraggingContext s_Context;
385
386 public EnvironmentPreviewDragger(DisplayWindow window, VisualElement dropArea)
387 {
388 m_Window = window;
389 m_DropArea = dropArea;
390 }
391
392 protected override void RegisterCallbacksOnTarget()
393 {
394 target.RegisterCallback<MouseDownEvent>(OnMouseDown);
395 target.RegisterCallback<MouseUpEvent>(OnMouseUp);
396 }
397
398 protected override void UnregisterCallbacksFromTarget()
399 {
400 target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
401 target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
402 }
403
404 void Release()
405 {
406 target.UnregisterCallback<MouseMoveEvent>(OnMouseMove);
407 s_Context?.Dispose();
408 target.ReleaseMouse();
409 s_Context = null;
410 }
411
412 void OnMouseDown(MouseDownEvent evt)
413 {
414 if (evt.button == 0)
415 {
416 target.CaptureMouse();
417 target.RegisterCallback<MouseMoveEvent>(OnMouseMove);
418 s_Context = m_Window.StartDragging(target, evt.mousePosition);
419 //do not stop event as we still need to propagate it to the ListView for selection
420 }
421 }
422
423 void OnMouseUp(MouseUpEvent evt)
424 {
425 if (evt.button != 0)
426 return;
427 if (m_DropArea.ContainsPoint(m_DropArea.WorldToLocal(Event.current.mousePosition)))
428 {
429 m_Window.EndDragging(s_Context, evt.mousePosition);
430 evt.StopPropagation();
431 }
432 Release();
433 }
434
435 void OnMouseMove(MouseMoveEvent evt)
436 {
437 evt.StopPropagation();
438 s_Context.UpdateCursorFollower(evt.mousePosition);
439 }
440 }
441
442 void IEnvironmentDisplayer.Repaint()
443 {
444 //can be unsync if library asset is destroy by user, so if null force sync
445 if (LookDev.currentContext.environmentLibrary == null)
446 m_LibraryField.value = null;
447
448 RefreshLibraryDisplay();
449 }
450
451 void OnFocus()
452 {
453 //OnFocus is called before OnEnable that open backend if not already opened, so only sync if backend is open
454 if (LookDev.open)
455 {
456 //If EnvironmentLibrary asset as been edited by the user (deletion),
457 //update all view to use null environment if it was not temporary ones
458 if (LookDev.currentContext.HasLibraryAssetChanged(m_LibraryField.value as EnvironmentLibrary))
459 {
460 ViewContext viewContext = LookDev.currentContext.GetViewContent(ViewIndex.First);
461 if (!(viewContext.environment?.isCubemapOnly ?? false))
462 OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.First, default);
463 viewContext = LookDev.currentContext.GetViewContent(ViewIndex.Second);
464 if (!(viewContext.environment?.isCubemapOnly ?? false))
465 OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.Second, default);
466 }
467
468 //If Cubemap asset as been edited by the user (deletion),
469 //update all views to use null environment if it was temporary ones
470 //and update all other views' environment to not use cubemap anymore
471 foreach (ViewContext viewContext in LookDev.currentContext.viewContexts)
472 {
473 if (viewContext.environment == null || !viewContext.environment.HasCubemapAssetChanged(viewContext.environment.cubemap))
474 continue;
475
476 if (viewContext.environment.isCubemapOnly)
477 viewContext.UpdateEnvironment(null);
478 else
479 viewContext.environment.cubemap = null;
480 }
481
482 ((IEnvironmentDisplayer)this).Repaint();
483 }
484 }
485
486 void FullRefreshEnvironmentList()
487 {
488 if (LookDev.currentContext.environmentLibrary != null)
489 LookDev.currentContext.FullReimportEnvironmentLibrary();
490
491 ((IEnvironmentDisplayer)this).Repaint();
492 }
493 }
494}