Update
This commit is contained in:
5
.idea/.idea.HALLUCINATE/.idea/workspace.xml
generated
5
.idea/.idea.HALLUCINATE/.idea/workspace.xml
generated
@@ -6,8 +6,9 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="f9183c68-daf0-43b8-be4c-fad79983f91b" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/UIManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/UIManager.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/Scripts/UI/SettingsController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/UI/SettingsController.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/UI/Global.uss" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Global.uss" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/UI/Settings.uxml" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/Settings.uxml" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -150,7 +151,7 @@
|
||||
<workItem from="1777443280908" duration="5223000" />
|
||||
<workItem from="1777484328779" duration="32427000" />
|
||||
<workItem from="1777568077522" duration="8613000" />
|
||||
<workItem from="1777604072510" duration="10566000" />
|
||||
<workItem from="1777604072510" duration="12524000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@@ -15,11 +15,15 @@ namespace Hallucinate.UI
|
||||
public class SettingsController : BaseUIController
|
||||
{
|
||||
private VisualElement _sidebar;
|
||||
private VisualElement _tabsColumn;
|
||||
private Label _tabTitle;
|
||||
private ScrollView _content;
|
||||
private Dictionary<string, Button> _tabButtons = new Dictionary<string, Button>();
|
||||
private string _activeTab = "GENERAL";
|
||||
|
||||
private Tween _hoverTimer;
|
||||
private bool _isExpanded;
|
||||
|
||||
// Advanced Mouse Metrics
|
||||
private Label _mouseMetricsLabel;
|
||||
|
||||
@@ -45,10 +49,15 @@ namespace Hallucinate.UI
|
||||
base.Initialize(uxmlRoot, manager);
|
||||
|
||||
_sidebar = root.Q<VisualElement>("Sidebar");
|
||||
_tabsColumn = root.Q<VisualElement>("TabsColumn");
|
||||
_tabTitle = root.Q<Label>("TabTitle");
|
||||
_content = root.Q<ScrollView>("SettingsContent");
|
||||
|
||||
// Global Volume Catch - Use TrickleDown to catch events before they are consumed by children (like ScrollViews)
|
||||
// Smart Sidebar Hover Logic
|
||||
_tabsColumn.RegisterCallback<PointerEnterEvent>(OnSidebarPointerEnter);
|
||||
_tabsColumn.RegisterCallback<PointerLeaveEvent>(OnSidebarPointerLeave);
|
||||
|
||||
// Global Volume Catch
|
||||
uiManager.Root.RegisterCallback<WheelEvent>(OnMouseWheel, TrickleDown.TrickleDown);
|
||||
SetupHierarchicalVolumeOverlay();
|
||||
|
||||
@@ -56,7 +65,6 @@ namespace Hallucinate.UI
|
||||
if (evt.target == root) uiManager.ToggleSettings();
|
||||
});
|
||||
|
||||
// Keyboard navigation for sliders
|
||||
root.RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
|
||||
SetupTab("GeneralTab", "GENERAL");
|
||||
@@ -68,28 +76,49 @@ namespace Hallucinate.UI
|
||||
if (closeBtn != null) closeBtn.clicked += () => uiManager.ToggleSettings();
|
||||
|
||||
_masterVol = PlayerPrefs.GetFloat("MasterVolume", 80f);
|
||||
|
||||
// Enforce Video Settings on start
|
||||
ApplyVideoSettings();
|
||||
|
||||
SwitchTab("GENERAL");
|
||||
}
|
||||
|
||||
private void OnSidebarPointerEnter(PointerEnterEvent evt)
|
||||
{
|
||||
_hoverTimer.Stop();
|
||||
// Bung ra ngay lập tức với animation cực mượt
|
||||
ExpandSidebar();
|
||||
}
|
||||
|
||||
private void OnSidebarPointerLeave(PointerLeaveEvent evt)
|
||||
{
|
||||
_hoverTimer.Stop();
|
||||
// Thu gọn ngay lập tức
|
||||
CollapseSidebar();
|
||||
}
|
||||
|
||||
private void ExpandSidebar()
|
||||
{
|
||||
if (_isExpanded) return;
|
||||
_isExpanded = true;
|
||||
_tabsColumn.AddToClassList("sidebar-expanded");
|
||||
// Animation 0.5s OutQuart cho cảm giác cao cấp
|
||||
Tween.Custom(_tabsColumn.resolvedStyle.width, 240f, duration: 0.5f, ease: Ease.OutQuart, onValueChange: val => _tabsColumn.style.width = val);
|
||||
}
|
||||
|
||||
private void CollapseSidebar()
|
||||
{
|
||||
if (!_isExpanded) return;
|
||||
_isExpanded = false;
|
||||
_tabsColumn.RemoveFromClassList("sidebar-expanded");
|
||||
Tween.Custom(_tabsColumn.resolvedStyle.width, 80f, duration: 0.45f, ease: Ease.OutQuart, onValueChange: val => _tabsColumn.style.width = val);
|
||||
}
|
||||
|
||||
private void ApplyVideoSettings()
|
||||
{
|
||||
// Frame Limiter
|
||||
int frameLimitIdx = PlayerPrefs.GetInt("FrameLimiter", 2);
|
||||
ApplyFrameLimit(frameLimitIdx);
|
||||
|
||||
// FPS Counter
|
||||
_fpsVisible = PlayerPrefs.GetInt("ShowFPS", 0) == 1;
|
||||
PerformanceOverlay.SetVisible(_fpsVisible);
|
||||
|
||||
// Background Dim
|
||||
float dim = PlayerPrefs.GetFloat("BackgroundDim", 50f);
|
||||
ApplyBackgroundDim(dim);
|
||||
|
||||
// Fullscreen
|
||||
bool isFull = PlayerPrefs.GetInt("Fullscreen", Screen.fullScreen ? 1 : 0) == 1;
|
||||
Screen.fullScreen = isFull;
|
||||
}
|
||||
@@ -109,59 +138,38 @@ namespace Hallucinate.UI
|
||||
private void ApplyBackgroundDim(float value)
|
||||
{
|
||||
PlayerPrefs.SetFloat("BackgroundDim", value);
|
||||
// Search for dim overlay in root
|
||||
var dimOverlay = uiManager.Root.Q<VisualElement>("BackgroundDimOverlay");
|
||||
if (dimOverlay != null)
|
||||
{
|
||||
dimOverlay.style.backgroundColor = new Color(0, 0, 0, value / 100f);
|
||||
}
|
||||
if (dimOverlay != null) dimOverlay.style.backgroundColor = new Color(0, 0, 0, value / 100f);
|
||||
}
|
||||
|
||||
private void SetupHierarchicalVolumeOverlay()
|
||||
{
|
||||
_volumeContainer = new VisualElement();
|
||||
_volumeContainer.name = "GlobalVolumeOverlay";
|
||||
_volumeContainer = new VisualElement { name = "GlobalVolumeOverlay" };
|
||||
_volumeContainer.style.position = Position.Absolute;
|
||||
_volumeContainer.style.right = 50;
|
||||
_volumeContainer.style.bottom = 50;
|
||||
_volumeContainer.style.width = 300;
|
||||
_volumeContainer.style.height = 300;
|
||||
_volumeContainer.style.right = 50; _volumeContainer.style.bottom = 50;
|
||||
_volumeContainer.style.width = 300; _volumeContainer.style.height = 300;
|
||||
_volumeContainer.style.display = DisplayStyle.None;
|
||||
_volumeContainer.pickingMode = PickingMode.Ignore;
|
||||
|
||||
// Add to UIManager root so it stays even when Settings is hidden
|
||||
uiManager.Root.Add(_volumeContainer);
|
||||
|
||||
// Master Ring (Bottom Right)
|
||||
_masterRing = CreateRing("Master", 120, cyan: true);
|
||||
_masterRing.style.right = 0;
|
||||
_masterRing.style.bottom = 0;
|
||||
_masterRing = CreateRing("Master", 120, cyan: true);
|
||||
_masterRing.style.right = 0; _masterRing.style.bottom = 0;
|
||||
_masterVolLabel = _masterRing.Q<Label>();
|
||||
_volumeContainer.Add(_masterRing);
|
||||
|
||||
// Sub Rings (Music, VFX, Player, UI)
|
||||
string[] subs = { "MusicVolume", "VFXVolume", "PlayerVolume", "UIVolume" };
|
||||
string[] shortNames = { "MUS", "VFX", "PLY", "UI" };
|
||||
|
||||
// Layout sub-rings in an arc around Master
|
||||
for (int i = 0; i < subs.Length; i++)
|
||||
{
|
||||
var ring = CreateRing(shortNames[i], 70, false);
|
||||
|
||||
// Angle 0 = Top, Angle 90 = Left
|
||||
float angle = (i * 30f) * Mathf.Deg2Rad;
|
||||
float radius = 140f;
|
||||
|
||||
// right moves element to the LEFT, bottom moves element UP
|
||||
// Starting from top (right=25, bottom=140+25) to left (right=140+25, bottom=25)
|
||||
ring.style.right = 25 + Mathf.Sin(angle) * radius;
|
||||
ring.style.bottom = 25 + Mathf.Cos(angle) * radius;
|
||||
|
||||
string key = subs[i];
|
||||
ring.RegisterCallback<PointerEnterEvent>(evt => _hoveredSubVolume = key);
|
||||
ring.RegisterCallback<PointerLeaveEvent>(evt => { if (_hoveredSubVolume == key) _hoveredSubVolume = null; });
|
||||
ring.pickingMode = PickingMode.Position; // Allow hover detection
|
||||
|
||||
ring.pickingMode = PickingMode.Position;
|
||||
_subRings[key] = (ring, ring.Q<Label>());
|
||||
_volumeContainer.Add(ring);
|
||||
}
|
||||
@@ -170,91 +178,49 @@ namespace Hallucinate.UI
|
||||
private VisualElement CreateRing(string text, float size, bool cyan)
|
||||
{
|
||||
var ring = new VisualElement();
|
||||
ring.style.width = size;
|
||||
ring.style.height = size;
|
||||
ring.style.width = size; ring.style.height = size;
|
||||
ring.style.backgroundColor = new Color(0, 0, 0, 0.85f);
|
||||
ring.style.borderTopLeftRadius = size / 2;
|
||||
ring.style.borderTopRightRadius = size / 2;
|
||||
ring.style.borderBottomLeftRadius = size / 2;
|
||||
ring.style.borderBottomRightRadius = size / 2;
|
||||
ring.style.borderTopWidth = 3;
|
||||
ring.style.borderBottomWidth = 3;
|
||||
ring.style.borderLeftWidth = 3;
|
||||
ring.style.borderRightWidth = 3;
|
||||
var radius = size / 2;
|
||||
ring.style.borderTopLeftRadius = radius; ring.style.borderTopRightRadius = radius;
|
||||
ring.style.borderBottomLeftRadius = radius; ring.style.borderBottomRightRadius = radius;
|
||||
ring.style.borderTopWidth = 3; ring.style.borderBottomWidth = 3;
|
||||
ring.style.borderLeftWidth = 3; ring.style.borderRightWidth = 3;
|
||||
ring.style.borderTopColor = ring.style.borderBottomColor = ring.style.borderLeftColor = ring.style.borderRightColor = cyan ? Color.cyan : new Color(0.7f, 0.7f, 0.7f);
|
||||
ring.style.justifyContent = Justify.Center;
|
||||
ring.style.alignItems = Align.Center;
|
||||
ring.style.justifyContent = Justify.Center; ring.style.alignItems = Align.Center;
|
||||
ring.style.position = Position.Absolute;
|
||||
|
||||
var label = new Label("80%");
|
||||
label.style.color = Color.white;
|
||||
label.style.fontSize = size * 0.25f;
|
||||
label.style.color = Color.white; label.style.fontSize = size * 0.25f;
|
||||
label.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
ring.Add(label);
|
||||
|
||||
var title = new Label(text);
|
||||
title.style.color = Color.gray;
|
||||
title.style.fontSize = size * 0.15f;
|
||||
title.style.position = Position.Absolute;
|
||||
title.style.bottom = size * 0.15f;
|
||||
title.style.color = Color.gray; title.style.fontSize = size * 0.15f;
|
||||
title.style.position = Position.Absolute; title.style.bottom = size * 0.15f;
|
||||
ring.Add(title);
|
||||
|
||||
return ring;
|
||||
}
|
||||
|
||||
private void OnMouseWheel(WheelEvent evt)
|
||||
{
|
||||
// Debug Log to see if event is even reaching here
|
||||
Debug.Log($"[SettingsController] Mouse Wheel Detected. Settings Open: {uiManager.IsSettingsOpen}");
|
||||
|
||||
// 1. Do not control volume if we are in MainMenu (unless Settings is explicitly open)
|
||||
var mainMenuRoot = uiManager.Root.Q<VisualElement>("MainMenuRoot");
|
||||
bool isMainMenuVisible = mainMenuRoot != null && mainMenuRoot.style.display == DisplayStyle.Flex;
|
||||
|
||||
if (!uiManager.IsSettingsOpen && isMainMenuVisible)
|
||||
{
|
||||
Debug.Log("[SettingsController] Volume control suppressed: Currently at Main Menu.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Detect target and context
|
||||
if (!uiManager.IsSettingsOpen && isMainMenuVisible) return;
|
||||
VisualElement target = evt.target as VisualElement;
|
||||
bool isDirectUIInteraction = _hoveredSubVolume != null || (_hoveredSlider != null && _activeTab == "SOUND");
|
||||
|
||||
// 3. If NOT direct interaction, check for ScrollView to prevent accidental volume changes while scrolling lists
|
||||
if (!isDirectUIInteraction && target != null)
|
||||
{
|
||||
if (target is ScrollView || target.GetFirstAncestorOfType<ScrollView>() != null)
|
||||
{
|
||||
Debug.Log("[SettingsController] Ignoring volume scroll: Hovering a ScrollView.");
|
||||
return;
|
||||
}
|
||||
if (target is ScrollView || target.GetFirstAncestorOfType<ScrollView>() != null) return;
|
||||
}
|
||||
|
||||
// 4. Osu style volume control
|
||||
_overlayActiveCount++;
|
||||
ShowVolumeOverlay();
|
||||
|
||||
if (_hoveredSubVolume != null)
|
||||
{
|
||||
Debug.Log($"[SettingsController] Adjusting Sub Volume: {_hoveredSubVolume}");
|
||||
UpdateSubVolume(_hoveredSubVolume, -evt.delta.y * 2f);
|
||||
}
|
||||
if (_hoveredSubVolume != null) UpdateSubVolume(_hoveredSubVolume, -evt.delta.y * 2f);
|
||||
else if (_hoveredSlider != null && _activeTab == "SOUND")
|
||||
{
|
||||
// If hovering a slider in the Sound tab, adjust that
|
||||
float currentVal = _hoveredSlider.value;
|
||||
float step = (_sliderMax - _sliderMin) / 100f;
|
||||
float newVal = Mathf.Clamp(currentVal - (evt.delta.y * step * 5f), _sliderMin, _sliderMax);
|
||||
float newVal = Mathf.Clamp(_hoveredSlider.value - (evt.delta.y * step * 5f), _sliderMin, _sliderMax);
|
||||
_hoveredSlider.value = newVal;
|
||||
_hoveredOnChanged?.Invoke(newVal);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[SettingsController] Adjusting Master Volume.");
|
||||
UpdateMasterVolume(-evt.delta.y * 2f);
|
||||
}
|
||||
|
||||
else UpdateMasterVolume(-evt.delta.y * 2f);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
@@ -264,83 +230,50 @@ namespace Hallucinate.UI
|
||||
PlayerPrefs.SetFloat("MasterVolume", _masterVol);
|
||||
AudioManager.Instance?.SetVolume("MasterVolume", _masterVol);
|
||||
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
|
||||
|
||||
// Refresh Sound Tab UI if visible
|
||||
if (_activeTab == "SOUND") SwitchTab("SOUND");
|
||||
}
|
||||
|
||||
private void UpdateSubVolume(string key, float delta)
|
||||
{
|
||||
float current = PlayerPrefs.GetFloat(key, 80f);
|
||||
float newVal = Mathf.Clamp(current + delta, 0f, 100f);
|
||||
float newVal = Mathf.Clamp(PlayerPrefs.GetFloat(key, 80f) + delta, 0f, 100f);
|
||||
PlayerPrefs.SetFloat(key, newVal);
|
||||
AudioManager.Instance?.SetVolume(key, newVal);
|
||||
|
||||
if (_subRings.TryGetValue(key, out var data))
|
||||
data.label.text = $"{Mathf.RoundToInt(newVal)}%";
|
||||
|
||||
if (_subRings.TryGetValue(key, out var data)) data.label.text = $"{Mathf.RoundToInt(newVal)}%";
|
||||
if (_activeTab == "SOUND") SwitchTab("SOUND");
|
||||
}
|
||||
|
||||
private async void ShowVolumeOverlay()
|
||||
{
|
||||
Debug.Log("[SettingsController] Showing Volume Overlay.");
|
||||
// Ensure overlay is on top of other UI screens
|
||||
_volumeContainer.BringToFront();
|
||||
|
||||
// CRITICAL: Ensure Virtual Cursor is ALWAYS on top of the volume rings
|
||||
uiManager.Root.Q<VisualElement>("CursorLayer")?.BringToFront();
|
||||
|
||||
_volumeContainer.style.display = DisplayStyle.Flex;
|
||||
_volumeContainer.style.opacity = 1f;
|
||||
|
||||
// Refresh all sub labels
|
||||
foreach (var kvp in _subRings)
|
||||
{
|
||||
float val = PlayerPrefs.GetFloat(kvp.Key, 80f);
|
||||
kvp.Value.label.text = $"{Mathf.RoundToInt(val)}%";
|
||||
}
|
||||
foreach (var kvp in _subRings) kvp.Value.label.text = $"{Mathf.RoundToInt(PlayerPrefs.GetFloat(kvp.Key, 80f))}%";
|
||||
_masterVolLabel.text = $"{Mathf.RoundToInt(_masterVol)}%";
|
||||
|
||||
int currentId = _overlayActiveCount;
|
||||
await Task.Delay(3000); // Wait 3s as requested
|
||||
|
||||
// Only fade out if no new scroll activity happened
|
||||
await Task.Delay(3000);
|
||||
if (currentId == _overlayActiveCount && _hoveredSubVolume == null)
|
||||
{
|
||||
Debug.Log("[SettingsController] Fading out Volume Overlay.");
|
||||
Tween.Custom(1f, 0f, duration: 0.5f, onValueChange: val => _volumeContainer.style.opacity = val)
|
||||
.OnComplete(() => {
|
||||
if (_volumeContainer.style.opacity == 0f)
|
||||
_volumeContainer.style.display = DisplayStyle.None;
|
||||
});
|
||||
.OnComplete(() => { if (_volumeContainer.style.opacity == 0f) _volumeContainer.style.display = DisplayStyle.None; });
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTab(string btnName, string tabId)
|
||||
{
|
||||
var btn = root.Q<Button>(btnName);
|
||||
if (btn != null)
|
||||
{
|
||||
_tabButtons[tabId] = btn;
|
||||
btn.clicked += () => SwitchTab(tabId);
|
||||
}
|
||||
if (btn != null) { _tabButtons[tabId] = btn; btn.clicked += () => SwitchTab(tabId); }
|
||||
}
|
||||
|
||||
private void SwitchTab(string tabId)
|
||||
{
|
||||
_activeTab = tabId;
|
||||
_tabTitle.text = tabId;
|
||||
|
||||
_activeTab = tabId; _tabTitle.text = tabId;
|
||||
foreach (var kvp in _tabButtons)
|
||||
{
|
||||
if (kvp.Key == tabId) kvp.Value.AddToClassList("active-tab");
|
||||
else kvp.Value.RemoveFromClassList("active-tab");
|
||||
}
|
||||
|
||||
_content.Clear();
|
||||
_hoveredSlider = null;
|
||||
|
||||
_content.Clear(); _hoveredSlider = null;
|
||||
switch (tabId)
|
||||
{
|
||||
case "GENERAL": RenderGeneralTab(); break;
|
||||
@@ -350,113 +283,76 @@ namespace Hallucinate.UI
|
||||
}
|
||||
}
|
||||
|
||||
#region GENERAL TAB
|
||||
private void RenderGeneralTab()
|
||||
{
|
||||
_content.Add(CreateSection("ACCOUNT"));
|
||||
string username = PlayerPrefs.GetString("Username", "Guest");
|
||||
var userRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 10 } };
|
||||
var loggedInLabel = new Label("Logged in as: ");
|
||||
loggedInLabel.AddToClassList("text-body");
|
||||
var loggedInLabel = new Label("Logged in as: "); loggedInLabel.AddToClassList("text-body");
|
||||
userRow.Add(loggedInLabel);
|
||||
userRow.Add(new Label(username) { style = { color = Color.cyan, marginLeft = 5, unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
userRow.Add(new Label(PlayerPrefs.GetString("Username", "Guest")) { style = { color = Color.cyan, marginLeft = 5, unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
_content.Add(userRow);
|
||||
|
||||
_content.Add(CreateSection("LANGUAGE"));
|
||||
var langDropdown = new DropdownField(new List<string> { "English", "Tiếng Việt" },
|
||||
LocalizationManager.Instance?.CurrentLanguage == "vi" ? 1 : 0);
|
||||
var langDropdown = new DropdownField(new List<string> { "English", "Tiếng Việt" }, LocalizationManager.Instance?.CurrentLanguage == "vi" ? 1 : 0);
|
||||
langDropdown.AddToClassList("custom-dropdown");
|
||||
langDropdown.RegisterValueChangedCallback(evt => {
|
||||
LocalizationManager.Instance?.LoadLanguage(evt.newValue == "Tiếng Việt" ? "vi" : "en");
|
||||
SwitchTab("GENERAL");
|
||||
LocalizationManager.Instance?.LoadLanguage(evt.newValue == "Tiếng Việt" ? "vi" : "en"); SwitchTab("GENERAL");
|
||||
});
|
||||
_content.Add(langDropdown);
|
||||
|
||||
_content.Add(CreateSection("UPDATES"));
|
||||
var versionBox = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
||||
var versionLabel = new Label($"Version: {Application.version}");
|
||||
versionLabel.AddToClassList("text-body");
|
||||
var versionLabel = new Label($"Version: {Application.version}"); versionLabel.AddToClassList("text-body");
|
||||
versionBox.Add(versionLabel);
|
||||
var checkBtn = new Button { text = "CHECK FOR UPDATES" };
|
||||
checkBtn.AddToClassList("button-spring");
|
||||
var checkBtn = new Button { text = "CHECK FOR UPDATES" }; checkBtn.AddToClassList("button-spring");
|
||||
checkBtn.clicked += () => checkBtn.text = "UP TO DATE";
|
||||
versionBox.Add(checkBtn);
|
||||
_content.Add(versionBox);
|
||||
|
||||
_content.Add(CreateSection("CURSOR & MOUSE"));
|
||||
_content.Add(CreateSliderWithInput("Cursor Size", 10, 150, PlayerPrefs.GetFloat("CursorSize", 40), val => uiManager.SetCursorSize(val)));
|
||||
|
||||
var trailToggle = new Toggle("Enable Cursor Trail") { value = PlayerPrefs.GetInt("CursorTrail", 1) == 1 };
|
||||
trailToggle.RegisterValueChangedCallback(evt => uiManager.SetCursorTrail(evt.newValue));
|
||||
_content.Add(trailToggle);
|
||||
|
||||
var rippleToggle = new Toggle("Enable Ripple Effects") { value = PlayerPrefs.GetInt("CursorRipples", 1) == 1 };
|
||||
rippleToggle.RegisterValueChangedCallback(evt => uiManager.SetCursorRipples(evt.newValue));
|
||||
_content.Add(rippleToggle);
|
||||
|
||||
_content.Add(CreateSliderWithInput("Sensitivity", 0.1f, 5.0f, PlayerPrefs.GetFloat("MouseSensitivity", 1.0f), val => uiManager.SetMouseSensitivity(val)));
|
||||
|
||||
var rawInputToggle = new Toggle("Raw Input (Bypass Acceleration)") { value = true };
|
||||
_content.Add(rawInputToggle);
|
||||
|
||||
_content.Add(new Toggle("Raw Input (Bypass Acceleration)") { value = true });
|
||||
_mouseMetricsLabel = new Label("[(report: 0/sec latency: 0ms)]") { style = { fontSize = 11, color = Color.gray, marginTop = 5 } };
|
||||
_content.Add(_mouseMetricsLabel);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VIDEO TAB
|
||||
private void RenderVideoTab()
|
||||
{
|
||||
_content.Add(CreateSection("RENDERER"));
|
||||
|
||||
int currentFrameLimit = PlayerPrefs.GetInt("FrameLimiter", 2);
|
||||
var frameLimit = new DropdownField("Frame Limiter", new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, currentFrameLimit);
|
||||
frameLimit.RegisterValueChangedCallback(evt => {
|
||||
ApplyFrameLimit(frameLimit.index);
|
||||
});
|
||||
var frameLimit = new DropdownField("Frame Limiter", new List<string> { "VSync", "Power Saving", "Optimal", "Unlimited" }, PlayerPrefs.GetInt("FrameLimiter", 2));
|
||||
frameLimit.RegisterValueChangedCallback(evt => ApplyFrameLimit(frameLimit.index));
|
||||
_content.Add(frameLimit);
|
||||
|
||||
var fpsToggle = new Toggle("Show FPS Counter") { value = _fpsVisible };
|
||||
fpsToggle.RegisterValueChangedCallback(evt => {
|
||||
_fpsVisible = evt.newValue;
|
||||
PlayerPrefs.SetInt("ShowFPS", _fpsVisible ? 1 : 0);
|
||||
PerformanceOverlay.SetVisible(_fpsVisible);
|
||||
});
|
||||
fpsToggle.RegisterValueChangedCallback(evt => { _fpsVisible = evt.newValue; PlayerPrefs.SetInt("ShowFPS", _fpsVisible ? 1 : 0); PerformanceOverlay.SetVisible(_fpsVisible); });
|
||||
_content.Add(fpsToggle);
|
||||
|
||||
_content.Add(CreateSection("LAYOUT"));
|
||||
Resolution native = Screen.currentResolution;
|
||||
var resList = Screen.resolutions.Select(r => $"{r.width}x{r.height}").Distinct().Select(s => s == $"{native.width}x{native.height}" ? s + " (native)" : s).ToList();
|
||||
|
||||
// Find current res in list
|
||||
string currentResStr = $"{Screen.width}x{Screen.height}";
|
||||
int currentResIdx = resList.FindIndex(s => s.StartsWith(currentResStr));
|
||||
if (currentResIdx == -1) currentResIdx = resList.FindIndex(s => s.Contains("native"));
|
||||
|
||||
var resDropdown = new DropdownField("Resolution", resList, currentResIdx);
|
||||
resDropdown.RegisterValueChangedCallback(evt => {
|
||||
string[] parts = evt.newValue.Split(' ')[0].Split('x');
|
||||
int w = int.Parse(parts[0]);
|
||||
int h = int.Parse(parts[1]);
|
||||
int w = int.Parse(parts[0]), h = int.Parse(parts[1]);
|
||||
Screen.SetResolution(w, h, Screen.fullScreen);
|
||||
PlayerPrefs.SetInt("ScreenWidth", w);
|
||||
PlayerPrefs.SetInt("ScreenHeight", h);
|
||||
PlayerPrefs.SetInt("ScreenWidth", w); PlayerPrefs.SetInt("ScreenHeight", h);
|
||||
});
|
||||
_content.Add(resDropdown);
|
||||
|
||||
var fullToggle = new Toggle("Fullscreen Mode") { value = Screen.fullScreen };
|
||||
fullToggle.RegisterValueChangedCallback(evt => {
|
||||
Screen.fullScreen = evt.newValue;
|
||||
PlayerPrefs.SetInt("Fullscreen", evt.newValue ? 1 : 0);
|
||||
});
|
||||
fullToggle.RegisterValueChangedCallback(evt => { Screen.fullScreen = evt.newValue; PlayerPrefs.SetInt("Fullscreen", evt.newValue ? 1 : 0); });
|
||||
_content.Add(fullToggle);
|
||||
|
||||
_content.Add(CreateSliderWithInput("Background Dim", 0, 100, PlayerPrefs.GetFloat("BackgroundDim", 50), val => ApplyBackgroundDim(val)));
|
||||
_content.Add(CreateSliderWithInput("UI Scale", 0.5f, 2.0f, PlayerPrefs.GetFloat("UIScale", 1.0f), val => uiManager.SetUIScale(val)));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SOUND TAB
|
||||
private void RenderSoundTab()
|
||||
{
|
||||
_content.Add(CreateSection("AUDIO VOLUMES"));
|
||||
@@ -465,205 +361,82 @@ namespace Hallucinate.UI
|
||||
_content.Add(CreateAudioSlider("VFX", "VFXVolume"));
|
||||
_content.Add(CreateAudioSlider("Player", "PlayerVolume"));
|
||||
_content.Add(CreateAudioSlider("UI", "UIVolume"));
|
||||
|
||||
_content.Add(new Label("Use Scroll Wheel to control volume.") { style = { marginTop = 20, color = Color.gray, fontSize = 12 } });
|
||||
}
|
||||
|
||||
private VisualElement CreateAudioSlider(string label, string prefKey)
|
||||
{
|
||||
var sliderRow = CreateSliderWithInput(label, 0, 100, PlayerPrefs.GetFloat(prefKey, 80), val => {
|
||||
PlayerPrefs.SetFloat(prefKey, val);
|
||||
AudioManager.Instance?.SetVolume(prefKey, val);
|
||||
PlayerPrefs.SetFloat(prefKey, val); AudioManager.Instance?.SetVolume(prefKey, val);
|
||||
});
|
||||
|
||||
// Register wheel specifically on this row
|
||||
sliderRow.RegisterCallback<WheelEvent>(evt => {
|
||||
float current = PlayerPrefs.GetFloat(prefKey, 80f);
|
||||
float newVal = Mathf.Clamp(current - (evt.delta.y * 2f), 0f, 100f);
|
||||
PlayerPrefs.SetFloat(prefKey, newVal);
|
||||
AudioManager.Instance?.SetVolume(prefKey, newVal);
|
||||
|
||||
// Visual update only (to avoid heavy re-render of whole list)
|
||||
var slider = sliderRow.Q<Slider>();
|
||||
if (slider != null) slider.value = newVal;
|
||||
float newVal = Mathf.Clamp(PlayerPrefs.GetFloat(prefKey, 80f) - (evt.delta.y * 2f), 0f, 100f);
|
||||
PlayerPrefs.SetFloat(prefKey, newVal); AudioManager.Instance?.SetVolume(prefKey, newVal);
|
||||
var slider = sliderRow.Q<Slider>(); if (slider != null) slider.value = newVal;
|
||||
});
|
||||
|
||||
return sliderRow;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CONTROL TAB
|
||||
private void RenderControlTab()
|
||||
{
|
||||
_content.Add(CreateSection("KEY BINDINGS"));
|
||||
|
||||
if (uiManager.InputReader == null || uiManager.InputReader.InputActions == null)
|
||||
{
|
||||
var errorLabel = new Label("Input Actions not found.");
|
||||
errorLabel.AddToClassList("text-body");
|
||||
_content.Add(errorLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiManager.InputReader?.InputActions == null) { _content.Add(new Label("Input Actions not found.") { style = { color = Color.white } }); return; }
|
||||
foreach (var map in uiManager.InputReader.InputActions.actionMaps)
|
||||
{
|
||||
// Add a sub-header for each map
|
||||
var mapHeader = new Label(map.name.ToUpper());
|
||||
mapHeader.style.fontSize = 14;
|
||||
mapHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
mapHeader.style.color = Color.cyan;
|
||||
mapHeader.style.marginTop = 15;
|
||||
mapHeader.style.marginBottom = 5;
|
||||
var mapHeader = new Label(map.name.ToUpper()) { style = { fontSize = 14, unityFontStyleAndWeight = FontStyle.Bold, color = Color.cyan, marginTop = 15, marginBottom = 5 } };
|
||||
_content.Add(mapHeader);
|
||||
|
||||
foreach (var action in map.actions)
|
||||
{
|
||||
// Skip internal/complex actions that shouldn't be rebound manually
|
||||
if (action.name == "Look" || action.name == "Scroll" ||
|
||||
action.name == "Navigate" || action.name == "Point" || action.name == "Click") continue;
|
||||
|
||||
if (action.name == "Look" || action.name == "Scroll" || action.name == "Navigate" || action.name == "Point" || action.name == "Click") continue;
|
||||
if (action.bindings.Any(b => b.isComposite))
|
||||
{
|
||||
for (int i = 0; i < action.bindings.Count; i++)
|
||||
{
|
||||
if (action.bindings[i].isPartOfComposite)
|
||||
{
|
||||
if (action.bindings[i].groups.Contains("Keyboard&Mouse"))
|
||||
{
|
||||
string label = $"{action.name} {action.bindings[i].name}".ToUpper();
|
||||
_content.Add(CreateRebindRow(action, i, label));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action.bindings[i].isPartOfComposite && action.bindings[i].groups.Contains("Keyboard&Mouse"))
|
||||
_content.Add(CreateRebindRow(action, i, $"{action.name} {action.bindings[i].name}".ToUpper()));
|
||||
}
|
||||
else
|
||||
{
|
||||
int bindingIndex = action.bindings.ToList().FindIndex(b => b.groups.Contains("Keyboard&Mouse"));
|
||||
if (bindingIndex != -1)
|
||||
{
|
||||
_content.Add(CreateRebindRow(action, bindingIndex, action.name.ToUpper()));
|
||||
}
|
||||
int idx = action.bindings.ToList().FindIndex(b => b.groups.Contains("Keyboard&Mouse"));
|
||||
if (idx != -1) _content.Add(CreateRebindRow(action, idx, action.name.ToUpper()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resetBtn = new Button { text = "RESET ALL TO DEFAULT" };
|
||||
resetBtn.AddToClassList("button-spring");
|
||||
resetBtn.style.marginTop = 30;
|
||||
resetBtn.style.alignSelf = Align.Center;
|
||||
resetBtn.clicked += () => {
|
||||
uiManager.InputReader.ResetBindings();
|
||||
SwitchTab("CONTROL");
|
||||
};
|
||||
var resetBtn = new Button { text = "RESET ALL TO DEFAULT" }; resetBtn.AddToClassList("button-spring"); resetBtn.style.marginTop = 30; resetBtn.style.alignSelf = Align.Center;
|
||||
resetBtn.clicked += () => { uiManager.InputReader.ResetBindings(); SwitchTab("CONTROL"); };
|
||||
_content.Add(resetBtn);
|
||||
}
|
||||
|
||||
private VisualElement CreateRebindRow(UnityEngine.InputSystem.InputAction action, int bindingIndex, string labelText)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.AddToClassList("rebind-row");
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.justifyContent = Justify.SpaceBetween;
|
||||
row.style.alignItems = Align.Center;
|
||||
row.style.paddingTop = 10;
|
||||
row.style.paddingBottom = 10;
|
||||
row.style.borderBottomWidth = 1;
|
||||
row.style.borderBottomColor = new Color(1, 1, 1, 0.1f);
|
||||
|
||||
var label = new Label(labelText);
|
||||
label.AddToClassList("rebind-label");
|
||||
label.style.color = Color.white;
|
||||
row.Add(label);
|
||||
|
||||
var bindingDisplay = action.GetBindingDisplayString(bindingIndex);
|
||||
var btn = new Button { text = bindingDisplay.ToUpper() };
|
||||
btn.AddToClassList("rebind-button");
|
||||
btn.style.width = 150;
|
||||
|
||||
btn.clicked += () => StartRebinding(action, bindingIndex, btn);
|
||||
|
||||
row.Add(btn);
|
||||
var row = new VisualElement(); row.AddToClassList("rebind-row"); row.style.flexDirection = FlexDirection.Row; row.style.justifyContent = Justify.SpaceBetween; row.style.alignItems = Align.Center; row.style.paddingTop = row.style.paddingBottom = 10; row.style.borderBottomWidth = 1; row.style.borderBottomColor = new Color(1, 1, 1, 0.1f);
|
||||
var label = new Label(labelText); label.AddToClassList("rebind-label"); label.style.color = Color.white; row.Add(label);
|
||||
var btn = new Button { text = action.GetBindingDisplayString(bindingIndex).ToUpper() }; btn.AddToClassList("rebind-button"); btn.style.width = 150;
|
||||
btn.clicked += () => StartRebinding(action, bindingIndex, btn); row.Add(btn);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void StartRebinding(UnityEngine.InputSystem.InputAction action, int bindingIndex, Button btn)
|
||||
{
|
||||
btn.text = "> <"; // Minecraft style "waiting"
|
||||
btn.style.color = Color.yellow;
|
||||
|
||||
// Disable input while rebinding to avoid side effects
|
||||
action.actionMap.Disable();
|
||||
|
||||
var rebindOperation = action.PerformInteractiveRebinding(bindingIndex)
|
||||
.WithControlsExcluding("<Mouse>/position")
|
||||
.WithControlsExcluding("<Mouse>/delta")
|
||||
.WithControlsExcluding("<Keyboard>/escape") // Keep ESC for cancel if needed
|
||||
.OnMatchWaitForAnother(0.1f)
|
||||
.OnComplete(operation => {
|
||||
btn.text = action.GetBindingDisplayString(bindingIndex).ToUpper();
|
||||
btn.style.color = Color.white;
|
||||
operation.Dispose();
|
||||
action.actionMap.Enable();
|
||||
uiManager.InputReader.SaveBindings();
|
||||
})
|
||||
.OnCancel(operation => {
|
||||
btn.text = action.GetBindingDisplayString(bindingIndex).ToUpper();
|
||||
btn.style.color = Color.white;
|
||||
operation.Dispose();
|
||||
action.actionMap.Enable();
|
||||
});
|
||||
|
||||
rebindOperation.Start();
|
||||
btn.text = "> <"; btn.style.color = Color.yellow; action.actionMap.Disable();
|
||||
var op = action.PerformInteractiveRebinding(bindingIndex).WithControlsExcluding("<Mouse>/position").WithControlsExcluding("<Mouse>/delta").WithControlsExcluding("<Keyboard>/escape").OnMatchWaitForAnother(0.1f)
|
||||
.OnComplete(operation => { btn.text = action.GetBindingDisplayString(bindingIndex).ToUpper(); btn.style.color = Color.white; operation.Dispose(); action.actionMap.Enable(); uiManager.InputReader.SaveBindings(); })
|
||||
.OnCancel(operation => { btn.text = action.GetBindingDisplayString(bindingIndex).ToUpper(); btn.style.color = Color.white; operation.Dispose(); action.actionMap.Enable(); });
|
||||
op.Start();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private VisualElement CreateSection(string title)
|
||||
{
|
||||
var label = new Label(title);
|
||||
label.AddToClassList("setting-section-header");
|
||||
label.style.marginTop = 20;
|
||||
return label;
|
||||
}
|
||||
private VisualElement CreateSection(string title) { var label = new Label(title); label.AddToClassList("setting-section-header"); label.style.marginTop = 20; return label; }
|
||||
|
||||
private VisualElement CreateSliderWithInput(string labelText, float min, float max, float startVal, Action<float> OnValueChanged)
|
||||
{
|
||||
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 5, marginBottom = 5 } };
|
||||
var label = new Label(labelText) { style = { width = Length.Percent(35) } };
|
||||
label.AddToClassList("text-body");
|
||||
|
||||
var label = new Label(labelText) { style = { width = Length.Percent(35) } }; label.AddToClassList("text-body");
|
||||
var slider = new Slider(min, max) { value = startVal, style = { flexGrow = 1 } };
|
||||
var input = new TextField { value = startVal.ToString("F1"), style = { width = 50, marginLeft = 10 } };
|
||||
input.AddToClassList("input-field");
|
||||
|
||||
slider.RegisterCallback<PointerEnterEvent>(evt => {
|
||||
_hoveredSlider = slider;
|
||||
_hoveredOnChanged = OnValueChanged;
|
||||
_sliderMin = min;
|
||||
_sliderMax = max;
|
||||
});
|
||||
slider.RegisterCallback<PointerLeaveEvent>(evt => {
|
||||
if (_hoveredSlider == slider) {
|
||||
_hoveredSlider = null;
|
||||
_hoveredOnChanged = null;
|
||||
}
|
||||
});
|
||||
|
||||
slider.RegisterValueChangedCallback(evt => {
|
||||
float val = Mathf.Round(evt.newValue * 10f) / 10f;
|
||||
if (input.panel?.focusController?.focusedElement != input.ElementAt(0)) input.value = val.ToString("F1");
|
||||
OnValueChanged?.Invoke(val);
|
||||
});
|
||||
|
||||
input.RegisterValueChangedCallback(evt => {
|
||||
if (float.TryParse(evt.newValue, out float val)) {
|
||||
slider.value = Mathf.Clamp(val, min, max);
|
||||
OnValueChanged?.Invoke(slider.value);
|
||||
}
|
||||
});
|
||||
|
||||
row.Add(label);
|
||||
row.Add(slider);
|
||||
row.Add(input);
|
||||
return row;
|
||||
var input = new TextField { value = startVal.ToString("F1"), style = { width = 50, marginLeft = 10 } }; input.AddToClassList("input-field");
|
||||
slider.RegisterCallback<PointerEnterEvent>(evt => { _hoveredSlider = slider; _hoveredOnChanged = OnValueChanged; _sliderMin = min; _sliderMax = max; });
|
||||
slider.RegisterCallback<PointerLeaveEvent>(evt => { if (_hoveredSlider == slider) { _hoveredSlider = null; _hoveredOnChanged = null; } });
|
||||
slider.RegisterValueChangedCallback(evt => { float val = Mathf.Round(evt.newValue * 10f) / 10f; if (input.panel?.focusController?.focusedElement != input.ElementAt(0)) input.value = val.ToString("F1"); OnValueChanged?.Invoke(val); });
|
||||
input.RegisterValueChangedCallback(evt => { if (float.TryParse(evt.newValue, out float val)) { slider.value = Mathf.Clamp(val, min, max); OnValueChanged?.Invoke(slider.value); } });
|
||||
row.Add(label); row.Add(slider); row.Add(input); return row;
|
||||
}
|
||||
|
||||
private void OnKeyDown(KeyDownEvent evt)
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
font-size: 18px;
|
||||
color: #eeeeee;
|
||||
-unity-font-style: normal;
|
||||
/* Giúp chữ mượt hơn */
|
||||
-unity-text-outline-width: 0.1px;
|
||||
-unity-text-outline-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -136,8 +135,6 @@
|
||||
|
||||
.button-spring:active {
|
||||
scale: 0.92 0.92;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transition-duration: 50ms;
|
||||
}
|
||||
|
||||
.btn-settings { background-color: #7B6EE8; }
|
||||
@@ -147,11 +144,10 @@
|
||||
.btn-exit { background-color: #666666; }
|
||||
|
||||
/* ============================================================
|
||||
DROPDOWN & FIELDS (THE BIG FIX)
|
||||
DROPDOWN & FIELDS
|
||||
============================================================ */
|
||||
DropdownField, .unity-dropdown-field {
|
||||
margin-bottom: 12px;
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
DropdownField .unity-dropdown-field__label {
|
||||
@@ -169,7 +165,6 @@ DropdownField .unity-base-field__input {
|
||||
padding: 10px 15px;
|
||||
flex-grow: 1;
|
||||
color: #00ffcc;
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
DropdownField:hover .unity-base-field__input {
|
||||
@@ -177,16 +172,11 @@ DropdownField:hover .unity-base-field__input {
|
||||
border-color: #00ffcc;
|
||||
}
|
||||
|
||||
/* NHẮM VÀO CÁI MENU XỔ XUỐNG (POPUP) */
|
||||
/* Cần selector cực kỳ mạnh và ghi đè toàn bộ phân cấp */
|
||||
|
||||
/* Container chính của Popup - Lớp phủ toàn màn hình */
|
||||
.unity-base-dropdown {
|
||||
background-color: rgba(0, 0, 0, 0) !important; /* Phải để trong suốt để không tràn màn hình */
|
||||
background-color: rgba(0, 0, 0, 0) !important;
|
||||
border-width: 0 !important;
|
||||
}
|
||||
|
||||
/* Phần nền nội bộ của cái menu box thực tế */
|
||||
.unity-base-dropdown__container-inner {
|
||||
background-color: #0a0a0a !important;
|
||||
border-width: 2px !important;
|
||||
@@ -196,7 +186,6 @@ DropdownField:hover .unity-base-field__input {
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
/* Từng dòng Item */
|
||||
.unity-base-dropdown__item {
|
||||
padding: 12px 15px !important;
|
||||
background-color: transparent !important;
|
||||
@@ -208,7 +197,7 @@ DropdownField:hover .unity-base-field__input {
|
||||
color: #eeeeee !important;
|
||||
}
|
||||
|
||||
/* Đè hiệu ứng hover mặc định */
|
||||
|
||||
.unity-base-dropdown__item:hover {
|
||||
background-color: rgba(0, 255, 204, 0.15) !important;
|
||||
color: #00ffcc !important;
|
||||
@@ -218,7 +207,7 @@ DropdownField:hover .unity-base-field__input {
|
||||
color: #00ffcc !important;
|
||||
}
|
||||
|
||||
/* Item được chọn */
|
||||
|
||||
.unity-base-dropdown__item--selected {
|
||||
background-color: rgba(0, 255, 204, 0.1) !important;
|
||||
color: #00ffcc !important;
|
||||
@@ -229,17 +218,19 @@ DropdownField:hover .unity-base-field__input {
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SMART SIDEBAR (OSU STYLE OVERLAY)
|
||||
SMART SIDEBAR (STABLE OVERLAY SYSTEM)
|
||||
============================================================ */
|
||||
.sidebar-tabs-container {
|
||||
background-color: rgba(5, 5, 5, 0.95);
|
||||
background-color: rgba(15, 15, 15, 0.98);
|
||||
padding-top: 60px;
|
||||
flex-shrink: 0;
|
||||
border-right-width: 1px;
|
||||
border-right-width: 2px;
|
||||
border-right-color: rgba(0, 255, 204, 0.1);
|
||||
transition: width 0.3s ease-out-quad;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
position: absolute; /* Phải là absolute để lướt đè */
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.sidebar-collapsed {
|
||||
@@ -247,11 +238,7 @@ DropdownField:hover .unity-base-field__input {
|
||||
}
|
||||
|
||||
.sidebar-expanded {
|
||||
width: 240px;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
border-right-color: #00ffcc;
|
||||
border-right-width: 2px;
|
||||
}
|
||||
|
||||
.sidebar-tab {
|
||||
@@ -263,7 +250,11 @@ DropdownField:hover .unity-base-field__input {
|
||||
align-items: center;
|
||||
border-left-width: 4px;
|
||||
border-left-color: transparent;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.active-tab {
|
||||
background-color: rgba(0, 255, 204, 0.08);
|
||||
border-left-color: #00ffcc;
|
||||
}
|
||||
|
||||
.tab-icon-box {
|
||||
@@ -277,13 +268,12 @@ DropdownField:hover .unity-base-field__input {
|
||||
.tab-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: #ffffff; /* Placeholder color */
|
||||
background-color: #ffffff;
|
||||
-unity-background-image-tint-color: #ffffff;
|
||||
}
|
||||
|
||||
.active-tab .tab-icon {
|
||||
-unity-background-image-tint-color: #00ffcc;
|
||||
background-color: #00ffcc;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
@@ -293,7 +283,7 @@ DropdownField:hover .unity-base-field__input {
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.3s ease-out-quad;
|
||||
}
|
||||
|
||||
.sidebar-expanded .tab-label {
|
||||
@@ -301,29 +291,18 @@ DropdownField:hover .unity-base-field__input {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ICON-ONLY BACK BUTTON */
|
||||
.btn-icon-only {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 25px;
|
||||
margin: 15px auto;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-icon-only .tab-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PANELS
|
||||
============================================================ */
|
||||
.panel-glass {
|
||||
background-color: var(--color-surface);
|
||||
border-radius: 20px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
.settings-details-column {
|
||||
margin-left: 80px; /* CHỖ CHỪA CHO ICON */
|
||||
flex-grow: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
@@ -362,74 +341,10 @@ DropdownField:hover .unity-base-field__input {
|
||||
.border-accent { border-color: #00ffcc; }
|
||||
|
||||
/* ============================================================
|
||||
SETTINGS & REBINDING
|
||||
PANELS & OTHERS
|
||||
============================================================ */
|
||||
.sidebar-tab {
|
||||
height: 54px;
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
color: #888888;
|
||||
-unity-font-style: bold;
|
||||
font-size: 16px;
|
||||
border-left-width: 4px;
|
||||
border-left-color: transparent;
|
||||
padding-left: 24px;
|
||||
border-radius: 0;
|
||||
margin: 2px 0;
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.sidebar-tab:hover {
|
||||
color: #ffffff;
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.active-tab {
|
||||
color: #00ffcc;
|
||||
border-left-color: #00ffcc;
|
||||
background-color: rgba(0, 255, 204, 0.08);
|
||||
}
|
||||
|
||||
.rebind-row {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.rebind-label {
|
||||
font-size: 14px;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.rebind-button {
|
||||
width: 140px;
|
||||
height: 36px;
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #00ffcc;
|
||||
font-size: 13px;
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.rebind-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-color: #00ffcc;
|
||||
}
|
||||
|
||||
.setting-section-header {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #666666;
|
||||
-unity-font-style: bold;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.05);
|
||||
padding-bottom: 6px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.panel-glass { background-color: var(--color-surface); border-radius: 20px; }
|
||||
.rebind-row { flex-direction: row; justify-content: space-between; align-items: center; padding: 12px 16px; background-color: rgba(255, 255, 255, 0.02); border-radius: 10px; margin-bottom: 6px; }
|
||||
.rebind-label { font-size: 14px; color: #cccccc; }
|
||||
.rebind-button { width: 140px; height: 36px; background-color: rgba(255, 255, 255, 0.08); border-radius: 8px; border-width: 1px; border-color: rgba(255, 255, 255, 0.1); color: #00ffcc; font-size: 13px; -unity-font-style: bold; }
|
||||
.setting-section-header { margin-top: 24px; margin-bottom: 12px; font-size: 13px; color: #666666; -unity-font-style: bold; border-bottom-width: 1px; border-bottom-color: rgba(255, 255, 255, 0.05); padding-bottom: 6px; letter-spacing: 1px; }
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="False">
|
||||
<Style src="project:/Assets/UI/Global.uss" />
|
||||
<ui:VisualElement name="SettingsRoot" class="screen-root" picking-mode="Position" style="background-color: rgba(0, 0, 0, 0.7); justify-content: flex-start; align-items: stretch;">
|
||||
<ui:VisualElement name="Sidebar" picking-mode="Position" class="panel-glass border-accent" style="min-width: 500px; width: 40%; height: 100%; flex-direction: row; padding: 0; border-radius: 0 30px 30px 0; overflow: hidden; border-right-width: 3px; border-left-width: 0; border-top-width: 0; border-bottom-width: 0;">
|
||||
<!-- Tabs -->
|
||||
<ui:VisualElement name="TabsColumn" style="width: 180px; background-color: rgba(0, 0, 0, 0.4); padding-top: 60px; flex-shrink: 0; border-right-width: 1px; border-right-color: rgba(255, 255, 255, 0.05);">
|
||||
<ui:Button name="GeneralTab" text="GENERAL" class="sidebar-tab active-tab" />
|
||||
<ui:Button name="VideoTab" text="VIDEO" class="sidebar-tab" />
|
||||
<ui:Button name="SoundTab" text="SOUND" class="sidebar-tab" />
|
||||
<ui:Button name="ControlTab" text="CONTROL" class="sidebar-tab" />
|
||||
<ui:VisualElement name="Sidebar" picking-mode="Position" class="panel-glass border-accent" style="width: 40%; height: 100%; flex-direction: row; padding: 0; border-radius: 0 30px 30px 0; border-right-width: 3px; border-left-width: 0; border-top-width: 0; border-bottom-width: 0; min-width: 500px;">
|
||||
<!-- Tabs Column (The Smart Sidebar) -->
|
||||
<ui:VisualElement name="TabsColumn" class="sidebar-tabs-container sidebar-collapsed">
|
||||
<ui:Button name="GeneralTab" class="sidebar-tab active-tab">
|
||||
<ui:VisualElement class="tab-icon-box"><ui:VisualElement class="tab-icon" /></ui:VisualElement>
|
||||
<ui:Label text="GENERAL" class="tab-label" />
|
||||
</ui:Button>
|
||||
<ui:Button name="VideoTab" class="sidebar-tab">
|
||||
<ui:VisualElement class="tab-icon-box"><ui:VisualElement class="tab-icon" /></ui:VisualElement>
|
||||
<ui:Label text="VIDEO" class="tab-label" />
|
||||
</ui:Button>
|
||||
<ui:Button name="SoundTab" class="sidebar-tab">
|
||||
<ui:VisualElement class="tab-icon-box"><ui:VisualElement class="tab-icon" /></ui:VisualElement>
|
||||
<ui:Label text="SOUND" class="tab-label" />
|
||||
</ui:Button>
|
||||
<ui:Button name="ControlTab" class="sidebar-tab">
|
||||
<ui:VisualElement class="tab-icon-box"><ui:VisualElement class="tab-icon" /></ui:VisualElement>
|
||||
<ui:Label text="CONTROL" class="tab-label" />
|
||||
</ui:Button>
|
||||
|
||||
<ui:VisualElement style="flex-grow: 1;" />
|
||||
<ui:Button name="CloseSettingsBtn" text="BACK" class="button-spring btn-exit" style="margin: 20px;" />
|
||||
<ui:Button name="CloseSettingsBtn" class="button-spring btn-icon-only" style="margin: 20px;">
|
||||
<ui:VisualElement class="tab-icon" style="background-image: none;" />
|
||||
</ui:Button>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Details -->
|
||||
<ui:VisualElement name="DetailsColumn" style="flex-grow: 1; padding: 40px; min-width: 320px; width: auto;">
|
||||
<!-- Details Column -->
|
||||
<ui:VisualElement name="DetailsColumn" class="settings-details-column" style="min-width: 320px;">
|
||||
<ui:Label name="TabTitle" text="GENERAL" class="text-heading" />
|
||||
<ui:ScrollView name="SettingsContent" class="scroll-list" style="flex-grow: 1;">
|
||||
<!-- Content will be injected here -->
|
||||
|
||||
Reference in New Issue
Block a user