289 lines
12 KiB
C#
289 lines
12 KiB
C#
using Godot;
|
|
using System;
|
|
|
|
// The in-VR menu content: plain Godot Controls, rendered by MenuScreen into its SubViewport.
|
|
// Built in code (no .tscn) so the spike can instantiate it without XR. Actions are C# events;
|
|
// WorldPanel wires them to the game. Sized for a 960x640 viewport on a 0.36x0.24 m panel
|
|
// (~2.7 px/mm): 26 px body text ≈ 1.2° at 45 cm, 64 px buttons ≈ 24 mm touch targets.
|
|
public partial class MainMenu : PanelContainer
|
|
{
|
|
public const string CalibrationGroup = "calibration";
|
|
|
|
private const int FontSize = 26;
|
|
private const int ButtonHeight = 64;
|
|
private const int MaxLogLines = 40;
|
|
|
|
public event Action ReturnToSpawnRequested;
|
|
public event Action ResetPropsRequested;
|
|
public event Action PingRequested;
|
|
public event Action CloseRequested;
|
|
public event Action Clicked;
|
|
|
|
public TabContainer Tabs { get; private set; }
|
|
public Button ReturnToSpawnButton { get; private set; }
|
|
public Button RecalibrateButton { get; private set; }
|
|
public Button ResetPropsButton { get; private set; }
|
|
public Button PingButton { get; private set; }
|
|
public Button CloseButton { get; private set; }
|
|
public Button HeaderCloseButton { get; private set; }
|
|
public HSlider ClickVolumeSlider { get; private set; }
|
|
public ScrollContainer LogScroll { get; private set; }
|
|
|
|
private Label _fpsLabel;
|
|
private Label _calibrationStatus;
|
|
private VBoxContainer _logLines;
|
|
private double _refreshTimer;
|
|
|
|
public MainMenu()
|
|
{
|
|
Theme = BuildTheme();
|
|
BuildLayout();
|
|
}
|
|
|
|
public override void _Ready() => RefreshCalibration();
|
|
|
|
public override void _Process(double delta)
|
|
{
|
|
_refreshTimer -= delta;
|
|
if (_refreshTimer > 0)
|
|
return;
|
|
_refreshTimer = 0.25;
|
|
_fpsLabel.Text = $"{Engine.GetFramesPerSecond():0} fps";
|
|
RefreshCalibration();
|
|
}
|
|
|
|
// Contract with the body-calibration work: a node in group "calibration" exposing
|
|
// Recalibrate(). Absent → button disabled. We never create that node here.
|
|
public Node FindCalibration()
|
|
{
|
|
if (!IsInsideTree())
|
|
return null;
|
|
Node node = GetTree().GetFirstNodeInGroup(CalibrationGroup);
|
|
return node != null && node.HasMethod("Recalibrate") ? node : null;
|
|
}
|
|
|
|
public void RefreshCalibration()
|
|
{
|
|
bool available = FindCalibration() != null;
|
|
RecalibrateButton.Disabled = !available;
|
|
_calibrationStatus.Text = available ? "" : "Not available";
|
|
}
|
|
|
|
public void Log(string line)
|
|
{
|
|
_logLines.AddChild(new Label { Text = line, AutowrapMode = TextServer.AutowrapMode.WordSmart });
|
|
while (_logLines.GetChildCount() > MaxLogLines)
|
|
{
|
|
Node oldest = _logLines.GetChild(0);
|
|
_logLines.RemoveChild(oldest);
|
|
oldest.QueueFree();
|
|
}
|
|
}
|
|
|
|
private void BuildLayout()
|
|
{
|
|
VBoxContainer root = new();
|
|
root.AddThemeConstantOverride("separation", 10);
|
|
AddChild(root);
|
|
|
|
HBoxContainer header = new();
|
|
header.AddThemeConstantOverride("separation", 16);
|
|
root.AddChild(header);
|
|
Label title = new() { Text = "VR Messs", SizeFlagsHorizontal = SizeFlags.ExpandFill, VerticalAlignment = VerticalAlignment.Center };
|
|
title.AddThemeFontSizeOverride("font_size", 30);
|
|
header.AddChild(title);
|
|
_fpsLabel = new Label { Text = "-- fps", VerticalAlignment = VerticalAlignment.Center };
|
|
_fpsLabel.AddThemeColorOverride("font_color", new Color(0.6f, 0.68f, 0.8f));
|
|
header.AddChild(_fpsLabel);
|
|
HeaderCloseButton = MakeButton("Close", () => CloseRequested?.Invoke());
|
|
HeaderCloseButton.CustomMinimumSize = new Vector2(120, 52);
|
|
header.AddChild(HeaderCloseButton);
|
|
|
|
Tabs = new TabContainer { SizeFlagsVertical = SizeFlags.ExpandFill, TabFocusMode = FocusModeEnum.None };
|
|
Tabs.TabChanged += _ => Clicked?.Invoke();
|
|
root.AddChild(Tabs);
|
|
|
|
// Player
|
|
VBoxContainer player = MakePage("Player");
|
|
ReturnToSpawnButton = MakeButton("Return to spawn", () => ReturnToSpawnRequested?.Invoke());
|
|
player.AddChild(ReturnToSpawnButton);
|
|
HBoxContainer calibRow = new();
|
|
calibRow.AddThemeConstantOverride("separation", 16);
|
|
RecalibrateButton = MakeButton("Recalibrate body", Recalibrate);
|
|
RecalibrateButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
|
calibRow.AddChild(RecalibrateButton);
|
|
_calibrationStatus = new Label { VerticalAlignment = VerticalAlignment.Center };
|
|
_calibrationStatus.AddThemeColorOverride("font_color", new Color(0.6f, 0.6f, 0.65f));
|
|
calibRow.AddChild(_calibrationStatus);
|
|
player.AddChild(calibRow);
|
|
Tabs.AddChild(player);
|
|
|
|
// World
|
|
VBoxContainer world = MakePage("World");
|
|
ResetPropsButton = MakeButton("Reset props", () => ResetPropsRequested?.Invoke());
|
|
world.AddChild(ResetPropsButton);
|
|
PingButton = MakeButton("Ping sound", () => PingRequested?.Invoke());
|
|
world.AddChild(PingButton);
|
|
Tabs.AddChild(world);
|
|
|
|
// System
|
|
VBoxContainer system = MakePage("System");
|
|
HBoxContainer volumeRow = new();
|
|
volumeRow.AddThemeConstantOverride("separation", 20);
|
|
volumeRow.AddChild(new Label { Text = "Click volume", VerticalAlignment = VerticalAlignment.Center, CustomMinimumSize = new Vector2(200, 0) });
|
|
ClickVolumeSlider = new HSlider
|
|
{
|
|
MinValue = 0,
|
|
MaxValue = 100,
|
|
Step = 1,
|
|
Value = 70,
|
|
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
|
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
|
CustomMinimumSize = new Vector2(0, 48),
|
|
FocusMode = FocusModeEnum.None,
|
|
};
|
|
volumeRow.AddChild(ClickVolumeSlider);
|
|
system.AddChild(volumeRow);
|
|
CloseButton = MakeButton("Close menu", () => CloseRequested?.Invoke());
|
|
system.AddChild(CloseButton);
|
|
Label logTitle = new() { Text = "Log (swipe or thumbstick to scroll)" };
|
|
logTitle.AddThemeColorOverride("font_color", new Color(0.6f, 0.68f, 0.8f));
|
|
system.AddChild(logTitle);
|
|
LogScroll = new ScrollContainer
|
|
{
|
|
SizeFlagsVertical = SizeFlags.ExpandFill,
|
|
HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled,
|
|
};
|
|
_logLines = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
|
_logLines.AddThemeConstantOverride("separation", 2);
|
|
LogScroll.AddChild(_logLines);
|
|
system.AddChild(LogScroll);
|
|
Tabs.AddChild(system);
|
|
}
|
|
|
|
private void Recalibrate()
|
|
{
|
|
Node calibration = FindCalibration();
|
|
if (calibration == null)
|
|
{
|
|
RefreshCalibration();
|
|
return;
|
|
}
|
|
GD.Print("MainMenu: Recalibrate body → calibration.Recalibrate()");
|
|
calibration.Call("Recalibrate");
|
|
}
|
|
|
|
private static VBoxContainer MakePage(string title)
|
|
{
|
|
VBoxContainer page = new() { Name = title };
|
|
page.AddThemeConstantOverride("separation", 14);
|
|
return page;
|
|
}
|
|
|
|
private Button MakeButton(string text, Action action)
|
|
{
|
|
Button button = new()
|
|
{
|
|
Text = text,
|
|
CustomMinimumSize = new Vector2(0, ButtonHeight),
|
|
FocusMode = FocusModeEnum.None,
|
|
};
|
|
button.Pressed += () =>
|
|
{
|
|
Clicked?.Invoke();
|
|
// Deferred: actions may hide/move the panel; never do that from inside the
|
|
// input event MenuScreen is injecting.
|
|
Callable.From(action).CallDeferred();
|
|
};
|
|
return button;
|
|
}
|
|
|
|
private static StyleBoxFlat Box(Color bg, int radius, float marginX, float marginY, Color? border = null, int borderWidth = 0)
|
|
{
|
|
StyleBoxFlat box = new() { BgColor = bg };
|
|
box.SetCornerRadiusAll(radius);
|
|
box.ContentMarginLeft = marginX;
|
|
box.ContentMarginRight = marginX;
|
|
box.ContentMarginTop = marginY;
|
|
box.ContentMarginBottom = marginY;
|
|
if (border is Color c)
|
|
{
|
|
box.BorderColor = c;
|
|
box.SetBorderWidthAll(borderWidth);
|
|
}
|
|
return box;
|
|
}
|
|
|
|
private static Theme BuildTheme()
|
|
{
|
|
Color accent = new(0.35f, 0.6f, 0.95f);
|
|
Theme theme = new() { DefaultFontSize = FontSize };
|
|
|
|
theme.SetStylebox("panel", "PanelContainer", Box(new Color(0.07f, 0.08f, 0.11f), 0, 18, 14));
|
|
|
|
theme.SetStylebox("normal", "Button", Box(new Color(0.17f, 0.2f, 0.27f), 10, 20, 8));
|
|
theme.SetStylebox("hover", "Button", Box(new Color(0.24f, 0.31f, 0.44f), 10, 20, 8, accent, 3));
|
|
theme.SetStylebox("pressed", "Button", Box(new Color(0.33f, 0.5f, 0.78f), 10, 20, 8, new Color(0.7f, 0.85f, 1f), 3));
|
|
theme.SetStylebox("hover_pressed", "Button", Box(new Color(0.33f, 0.5f, 0.78f), 10, 20, 8, new Color(0.7f, 0.85f, 1f), 3));
|
|
theme.SetStylebox("disabled", "Button", Box(new Color(0.11f, 0.12f, 0.15f), 10, 20, 8));
|
|
theme.SetStylebox("focus", "Button", new StyleBoxEmpty());
|
|
theme.SetColor("font_color", "Button", new Color(0.92f, 0.94f, 0.98f));
|
|
theme.SetColor("font_hover_color", "Button", Colors.White);
|
|
theme.SetColor("font_pressed_color", "Button", Colors.White);
|
|
theme.SetColor("font_disabled_color", "Button", new Color(0.45f, 0.47f, 0.52f));
|
|
|
|
theme.SetStylebox("panel", "TabContainer", Box(new Color(0.1f, 0.12f, 0.16f), 10, 18, 16));
|
|
StyleBoxFlat tabSelected = Box(new Color(0.1f, 0.12f, 0.16f), 0, 28, 12);
|
|
tabSelected.CornerRadiusTopLeft = 10;
|
|
tabSelected.CornerRadiusTopRight = 10;
|
|
tabSelected.BorderColor = accent;
|
|
tabSelected.BorderWidthTop = 4;
|
|
theme.SetStylebox("tab_selected", "TabContainer", tabSelected);
|
|
StyleBoxFlat tabUnselected = Box(new Color(0.13f, 0.15f, 0.2f), 0, 28, 12);
|
|
tabUnselected.CornerRadiusTopLeft = 10;
|
|
tabUnselected.CornerRadiusTopRight = 10;
|
|
theme.SetStylebox("tab_unselected", "TabContainer", tabUnselected);
|
|
StyleBoxFlat tabHovered = Box(new Color(0.2f, 0.26f, 0.37f), 0, 28, 12);
|
|
tabHovered.CornerRadiusTopLeft = 10;
|
|
tabHovered.CornerRadiusTopRight = 10;
|
|
theme.SetStylebox("tab_hovered", "TabContainer", tabHovered);
|
|
theme.SetStylebox("tab_focus", "TabContainer", new StyleBoxEmpty());
|
|
theme.SetConstant("tab_separation", "TabContainer", 6);
|
|
theme.SetFontSize("font_size", "TabContainer", FontSize);
|
|
|
|
// Slider: thick track, big round grabber (default icons are ~16 px — too small in VR).
|
|
StyleBoxFlat track = Box(new Color(0.17f, 0.2f, 0.27f), 6, 0, 6);
|
|
theme.SetStylebox("slider", "HSlider", track);
|
|
theme.SetStylebox("grabber_area", "HSlider", Box(accent, 6, 0, 6));
|
|
theme.SetStylebox("grabber_area_highlight", "HSlider", Box(accent.Lightened(0.2f), 6, 0, 6));
|
|
theme.SetIcon("grabber", "HSlider", Circle(36, new Color(0.9f, 0.94f, 1f)));
|
|
theme.SetIcon("grabber_highlight", "HSlider", Circle(36, Colors.White));
|
|
theme.SetIcon("grabber_disabled", "HSlider", Circle(36, new Color(0.4f, 0.4f, 0.45f)));
|
|
|
|
// Wide scrollbar so it is touchable.
|
|
theme.SetStylebox("scroll", "VScrollBar", Box(new Color(0.13f, 0.15f, 0.2f), 8, 14, 0));
|
|
theme.SetStylebox("grabber", "VScrollBar", Box(new Color(0.3f, 0.36f, 0.48f), 8, 14, 0));
|
|
theme.SetStylebox("grabber_highlight", "VScrollBar", Box(new Color(0.4f, 0.5f, 0.68f), 8, 14, 0));
|
|
theme.SetStylebox("grabber_pressed", "VScrollBar", Box(accent, 8, 14, 0));
|
|
|
|
theme.SetColor("font_color", "Label", new Color(0.88f, 0.9f, 0.95f));
|
|
return theme;
|
|
}
|
|
|
|
private static ImageTexture Circle(int size, Color color)
|
|
{
|
|
Image image = Image.CreateEmpty(size, size, false, Image.Format.Rgba8);
|
|
float r = size * 0.5f;
|
|
for (int y = 0; y < size; y++)
|
|
{
|
|
for (int x = 0; x < size; x++)
|
|
{
|
|
float d = new Vector2(x + 0.5f - r, y + 0.5f - r).Length();
|
|
float a = Mathf.Clamp(r - d, 0f, 1f); // 1 px anti-aliased edge
|
|
image.SetPixel(x, y, new Color(color, a));
|
|
}
|
|
}
|
|
return ImageTexture.CreateFromImage(image);
|
|
}
|
|
}
|