diff --git a/source/LibRender2/LibRender2.csproj b/source/LibRender2/LibRender2.csproj
index dcea6d70e..c47eef0ee 100644
--- a/source/LibRender2/LibRender2.csproj
+++ b/source/LibRender2/LibRender2.csproj
@@ -100,6 +100,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -142,7 +156,7 @@
-
+
diff --git a/source/LibRender2/Menu/AbstractMenu.cs b/source/LibRender2/Menu/AbstractMenu.cs
index c65cb5292..327a4d55e 100644
--- a/source/LibRender2/Menu/AbstractMenu.cs
+++ b/source/LibRender2/Menu/AbstractMenu.cs
@@ -32,6 +32,9 @@
using OpenBveApi;
using OpenBveApi.Input;
using OpenBveApi.Interface;
+using OpenBveApi.Hosts;
+using LibRender2.Viewports;
+using OpenBveApi.Objects;
namespace LibRender2.Menu
{
@@ -39,13 +42,13 @@ namespace LibRender2.Menu
public abstract class AbstractMenu
{
/// The color used for the overlay
- public readonly Color128 overlayColor = new Color128(0.0f, 0.0f, 0.0f, 0.2f);
+ public readonly Color128 overlayColor = new Color128(0.0f, 0.0f, 0.0f, 0.45f);
/// The color used for the menu background
- public readonly Color128 backgroundColor = Color128.Black;
+ public readonly Color128 backgroundColor = new Color128(0.08f, 0.09f, 0.11f, 0.94f);
/// The color used to draw the highlight over a selected item
- public readonly Color128 highlightColor = Color128.Orange;
+ public readonly Color128 highlightColor = new Color128(0.0f, 0.47f, 0.83f, 1.0f);
/// The color used to draw the highlight over a selected folder
public readonly Color128 folderHighlightColor = new Color128(0.0f, 0.69f, 1.0f, 1.0f);
@@ -54,13 +57,13 @@ public abstract class AbstractMenu
public readonly Color128 routeHighlightColor = new Color128(0.0f, 1.0f, 0.69f, 1.0f);
/// The color used for caption text
- public static readonly Color128 ColourCaption = new Color128(0.750f, 0.750f, 0.875f, 1.0f);
+ public static readonly Color128 ColourCaption = new Color128(0.58f, 0.75f, 0.98f, 1.0f);
/// The color used to draw dimmed text
- public static readonly Color128 ColourDimmed = new Color128(1.000f, 1.000f, 1.000f, 0.5f);
+ public static readonly Color128 ColourDimmed = new Color128(0.6f, 0.64f, 0.7f, 0.65f);
/// The color used to draw highlighted text
- public static readonly Color128 ColourHighlight = Color128.Black;
+ public static readonly Color128 ColourHighlight = Color128.White;
/// The color used to draw normal text
public static readonly Color128 ColourNormal = Color128.White;
@@ -107,6 +110,255 @@ public abstract class AbstractMenu
/// The controls within the menu
public List menuControls = new List();
+ /// The tab container for the options panel
+ public GLTabContainer OptionsTabContainer;
+
+ public void InitializeOptionsTabContainer(HostApplication host)
+ {
+ if (OptionsTabContainer != null) return;
+
+ OptionsTabContainer = new GLTabContainer(Renderer);
+ OptionsTabContainer.IsVisible = false;
+
+ menuControls.Add(OptionsTabContainer);
+
+ var displayTab = new GLVBoxContainer(Renderer) { Spacing = 8f };
+ var qualityTab = new GLVBoxContainer(Renderer) { Spacing = 8f };
+ var otherTab = new GLVBoxContainer(Renderer) { Spacing = 8f };
+
+ OptionsTabContainer.AddTab("Display", displayTab);
+ OptionsTabContainer.AddTab("Quality", qualityTab);
+ OptionsTabContainer.AddTab("Other", otherTab);
+
+ // Populate Display Tab
+ var resRow = CreateRow("Resolution");
+ var resDropdown = new GLDropdown(Renderer);
+ foreach (var r in Renderer.Screen.AvailableResolutions)
+ {
+ resDropdown.Items.Add(r.ToString());
+ }
+ for (int i = 0; i < Renderer.Screen.AvailableResolutions.Count; i++)
+ {
+ var r = Renderer.Screen.AvailableResolutions[i];
+ if (System.Math.Abs(r.Width - Renderer.Screen.Width) < 5 && System.Math.Abs(r.Height - Renderer.Screen.Height) < 5)
+ {
+ resDropdown.SelectedIndex = i;
+ break;
+ }
+ }
+ resDropdown.SelectionChanged += (s, e) =>
+ {
+ if (resDropdown.SelectedIndex >= 0 && resDropdown.SelectedIndex < Renderer.Screen.AvailableResolutions.Count)
+ {
+ var res = Renderer.Screen.AvailableResolutions[resDropdown.SelectedIndex];
+ Renderer.SetWindowSize((int)(res.Width * Renderer.ScaleFactor.X), (int)(res.Height * Renderer.ScaleFactor.Y));
+ if (!CurrentOptions.FullscreenMode)
+ {
+ CurrentOptions.WindowWidth = res.Width;
+ CurrentOptions.WindowHeight = res.Height;
+ }
+ ComputePosition();
+ }
+ };
+ resRow.Children.Add(resDropdown);
+ displayTab.Children.Add(resRow);
+
+ var fsRow = CreateRow("Fullscreen");
+ var fsToggle = new GLToggle(Renderer) { Checked = CurrentOptions.FullscreenMode, Text = "" };
+ fsToggle.ValueChanged += (s, e) =>
+ {
+ CurrentOptions.FullscreenMode = fsToggle.Checked;
+ if (!CurrentOptions.FullscreenMode)
+ {
+ Renderer.SetWindowState(OpenTK.WindowState.Normal);
+ OpenTK.DisplayDevice.Default.RestoreResolution();
+ }
+ else
+ {
+ var resolutions = OpenTK.DisplayDevice.Default.AvailableResolutions;
+ foreach (var res in resolutions)
+ {
+ if (res.Width == Renderer.Screen.Width / Renderer.ScaleFactor.X && res.Height == Renderer.Screen.Height / Renderer.ScaleFactor.Y)
+ {
+ try
+ {
+ OpenTK.DisplayDevice.Default.RestoreResolution();
+ OpenTK.DisplayDevice.Default.ChangeResolution(res);
+ Renderer.SetWindowState(OpenTK.WindowState.Fullscreen);
+ Renderer.SetWindowSize((int)(res.Width * Renderer.ScaleFactor.X), (int)(res.Height * Renderer.ScaleFactor.Y));
+ break;
+ }
+ catch { }
+ }
+ }
+ }
+ ComputePosition();
+ };
+ fsRow.Children.Add(fsToggle);
+ displayTab.Children.Add(fsRow);
+
+ if (host == HostApplication.OpenBve)
+ {
+ var scaleRow = CreateRow("UI Scale");
+ var scaleDropdown = new GLDropdown(Renderer);
+ scaleDropdown.Items.AddRange(new[] { "1x", "2x", "3x", "4x", "5x", "6x" });
+ scaleDropdown.SelectedIndex = CurrentOptions.UserInterfaceScaleFactor - 1;
+ scaleDropdown.SelectionChanged += (s, e) =>
+ {
+ CurrentOptions.UserInterfaceScaleFactor = scaleDropdown.SelectedIndex + 1;
+ };
+ scaleRow.Children.Add(scaleDropdown);
+ displayTab.Children.Add(scaleRow);
+ }
+
+ // Populate Quality Tab
+ var interpRow = CreateRow("Interpolation");
+ var interpDropdown = new GLDropdown(Renderer);
+ interpDropdown.Items.AddRange(new[] { "Nearest", "Bilinear", "NearestMipmap", "BilinearMipmap", "TrilinearMipmap", "Anisotropic" });
+ interpDropdown.SelectedIndex = (int)CurrentOptions.Interpolation;
+ interpDropdown.SelectionChanged += (s, e) =>
+ {
+ CurrentOptions.Interpolation = (InterpolationMode)interpDropdown.SelectedIndex;
+ Renderer.TextureManager.UnloadAllTextures(true);
+ Renderer.TextureManager.LoadAllTextures();
+ };
+ interpRow.Children.Add(interpDropdown);
+ qualityTab.Children.Add(interpRow);
+
+ var anisoRow = CreateRow("Anisotropic Level");
+ var anisoDropdown = new GLDropdown(Renderer);
+ anisoDropdown.Items.AddRange(new[] { "0", "2", "4", "8", "16" });
+ int currentAniso = CurrentOptions.AnisotropicFilteringLevel;
+ int selectAnisoIdx = anisoDropdown.Items.IndexOf(currentAniso.ToString());
+ if (selectAnisoIdx >= 0) anisoDropdown.SelectedIndex = selectAnisoIdx;
+ anisoDropdown.SelectionChanged += (s, e) =>
+ {
+ CurrentOptions.AnisotropicFilteringLevel = int.Parse(anisoDropdown.Items[anisoDropdown.SelectedIndex]);
+ Renderer.TextureManager.UnloadAllTextures(true);
+ Renderer.TextureManager.LoadAllTextures();
+ };
+ anisoRow.Children.Add(anisoDropdown);
+ qualityTab.Children.Add(anisoRow);
+
+ var aaRow = CreateRow("Antialiasing Level");
+ var aaDropdown = new GLDropdown(Renderer);
+ aaDropdown.Items.AddRange(new[] { "0", "2", "4", "8", "16" });
+ int currentAA = CurrentOptions.AntiAliasingLevel;
+ int selectAAIdx = aaDropdown.Items.IndexOf(currentAA.ToString());
+ if (selectAAIdx >= 0) aaDropdown.SelectedIndex = selectAAIdx;
+ aaDropdown.SelectionChanged += (s, e) =>
+ {
+ CurrentOptions.AntiAliasingLevel = int.Parse(aaDropdown.Items[aaDropdown.SelectedIndex]);
+ };
+ aaRow.Children.Add(aaDropdown);
+ qualityTab.Children.Add(aaRow);
+
+ var transRow = CreateRow("Transparency Quality");
+ var transDropdown = new GLDropdown(Renderer);
+ transDropdown.Items.AddRange(new[] { "Sharp", "Intermediate", "Smooth" });
+ transDropdown.SelectedIndex = (int)CurrentOptions.TransparencyMode;
+ transDropdown.SelectionChanged += (s, e) =>
+ {
+ CurrentOptions.TransparencyMode = (TransparencyMode)transDropdown.SelectedIndex;
+ };
+ transRow.Children.Add(transDropdown);
+ qualityTab.Children.Add(transRow);
+
+ var distRow = CreateRow("Viewing Distance");
+ var distUpDown = new GLNumericUpDown(Renderer) { Minimum = 10f, Maximum = 9999f, Increment = 50f, Value = CurrentOptions.ViewingDistance };
+ distUpDown.ValueChanged += (s, e) =>
+ {
+ CurrentOptions.ViewingDistance = (int)distUpDown.Value;
+ Renderer.UpdateViewport(ViewportChangeMode.ChangeToScenery);
+ if (Renderer.CameraTrackFollower != null)
+ {
+ Renderer.UpdateViewingDistances(CurrentOptions.ViewingDistance);
+ }
+ OnOptionChanged(OptionType.ViewingDistance);
+ };
+ distRow.Children.Add(distUpDown);
+ qualityTab.Children.Add(distRow);
+
+ var clipRow = CreateRow("Near Clip");
+ var clipUpDown = new GLNumericUpDown(Renderer) { Minimum = 0.001f, Maximum = 1f, Increment = 0.01f, Value = (float)CurrentOptions.NearClipBase };
+ clipUpDown.ValueChanged += (s, e) =>
+ {
+ CurrentOptions.NearClipBase = clipUpDown.Value;
+ Renderer.UpdateViewport(ViewportChangeMode.ChangeToScenery);
+ OnOptionChanged(OptionType.NearClip);
+ };
+ clipRow.Children.Add(clipUpDown);
+ qualityTab.Children.Add(clipRow);
+
+ // Populate Other Tab
+ var xParserRow = CreateRow("X Parser");
+ var xParserDropdown = new GLDropdown(Renderer);
+ xParserDropdown.Items.AddRange(new[] { "Original", "NewXParser", "Assimp" });
+ xParserDropdown.SelectedIndex = (int)CurrentOptions.CurrentXParser;
+ xParserDropdown.SelectionChanged += (s, e) =>
+ {
+ CurrentOptions.CurrentXParser = (XParsers)xParserDropdown.SelectedIndex;
+ };
+ xParserRow.Children.Add(xParserDropdown);
+ otherTab.Children.Add(xParserRow);
+
+ var objParserRow = CreateRow("Obj Parser");
+ var objParserDropdown = new GLDropdown(Renderer);
+ objParserDropdown.Items.AddRange(new[] { "Original", "Assimp" });
+ objParserDropdown.SelectedIndex = (int)CurrentOptions.CurrentObjParser;
+ objParserDropdown.SelectionChanged += (s, e) =>
+ {
+ CurrentOptions.CurrentObjParser = (ObjParsers)objParserDropdown.SelectedIndex;
+ };
+ objParserRow.Children.Add(objParserDropdown);
+ otherTab.Children.Add(objParserRow);
+
+ if (host == HostApplication.ObjectViewer)
+ {
+ var reloadRow = CreateRow("Auto Reload Obj");
+ var reloadToggle = new GLToggle(Renderer) { Checked = CurrentOptions.AutoReloadObjects, Text = "" };
+ reloadToggle.ValueChanged += (s, e) =>
+ {
+ CurrentOptions.AutoReloadObjects = reloadToggle.Checked;
+ };
+ reloadRow.Children.Add(reloadToggle);
+ otherTab.Children.Add(reloadRow);
+ }
+
+ if (host == HostApplication.RouteViewer)
+ {
+ var logoRow = CreateRow("Show Logo");
+ var logoToggle = new GLToggle(Renderer) { Checked = CurrentOptions.LoadingLogo, Text = "" };
+ logoToggle.ValueChanged += (s, e) => { CurrentOptions.LoadingLogo = logoToggle.Checked; };
+ logoRow.Children.Add(logoToggle);
+ otherTab.Children.Add(logoRow);
+
+ var bgRow = CreateRow("Show Backgrounds");
+ var bgToggle = new GLToggle(Renderer) { Checked = CurrentOptions.LoadingBackground, Text = "" };
+ bgToggle.ValueChanged += (s, e) => { CurrentOptions.LoadingBackground = bgToggle.Checked; };
+ bgRow.Children.Add(bgToggle);
+ otherTab.Children.Add(bgRow);
+
+ var barRow = CreateRow("Show Progress Bar");
+ var barToggle = new GLToggle(Renderer) { Checked = CurrentOptions.LoadingProgressBar, Text = "" };
+ barToggle.ValueChanged += (s, e) => { CurrentOptions.LoadingProgressBar = barToggle.Checked; };
+ barRow.Children.Add(barToggle);
+ otherTab.Children.Add(barRow);
+ }
+ }
+
+ private GLHBoxContainer CreateRow(string name)
+ {
+ var row = new GLHBoxContainer(Renderer) { Spacing = 12f };
+ row.Size = new Vector2(300f, 24f);
+ var label = new Label(Renderer, name);
+ label.BackgroundColor = new Color128(0, 0, 0, 0);
+ // Let the label keep its natural measured width so long text never overflows
+ label.Size = new Vector2(System.Math.Max(label.Size.X, 24f), 24f);
+ row.Children.Add(label);
+ return row;
+ }
+
/// Whether the menu system is initialized
public bool IsInitialized = false;
@@ -155,6 +407,13 @@ public void PopMenu()
{ // if only one menu remaining...
Reset();
Renderer.CurrentInterface = InterfaceType.Normal; // return to simulation
+ if (IsSidebarMode)
+ {
+ SidebarVisible = false;
+ startOffset = currentOffset;
+ animationElapsed = 0.0;
+ isAnimating = true;
+ }
}
}
@@ -166,10 +425,159 @@ public virtual void ProcessMouseScroll(int Scroll)
{
return;
}
+ if (CurrMenu >= 0 && Menus[CurrMenu].Type == MenuType.Options && OptionsTabContainer != null && OptionsTabContainer.IsVisible)
+ {
+ OptionsTabContainer.MouseWheel(Scroll);
+ return;
+ }
// Load the current menu
Menus[CurrMenu].ProcessScroll(Scroll, visibleItems);
}
+ /// Whether the menu system is configured as a sliding sidebar on the left
+ public bool IsSidebarMode = true;
+
+ /// Whether the sidebar is currently open/visible
+ public bool SidebarVisible = false;
+
+ /// The width of the sidebar as a fraction of screen width
+ public double SidebarWidthRatio = 0.25;
+
+ /// Gets the current calculated width of the sidebar panel
+ public double SidebarWidth => Renderer.Screen.Width * SidebarWidthRatio;
+
+ public double currentOffset = -99999.0;
+ public double animationElapsed = 0.0;
+ public double animationDuration = 0.4;
+ public double startOffset = 0.0;
+ public bool isAnimating = false;
+
+ public bool isScrubbing = false;
+ public MenuOption scrubbingOption = null;
+ public int lastMouseX = 0;
+
+ public Vector4 GetToggleButtonRect()
+ {
+ double buttonX = IsSidebarMode ? (currentOffset < -90000.0 ? (SidebarVisible ? SidebarWidth : 0) : (currentOffset + SidebarWidth)) : 0;
+ double buttonY = Renderer.Screen.Height / 2.0 - 20;
+ return new Vector4((float)buttonX, (float)buttonY, 24f, 40f);
+ }
+
+ /// The width (in px) of the grab/hover zone on the sidebar's right edge
+ private const float SidebarResizeZone = 8f;
+
+ /// Whether the user is currently dragging the sidebar edge to resize it
+ public bool isResizingSidebar = false;
+
+ /// Whether the mouse is currently hovering the sidebar resize edge (0..1 for highlight fade)
+ public float sidebarResizeHover = 0f;
+
+ /// The screen X co-ordinate of the sidebar's right edge, or -1 if not currently shown
+ public double SidebarEdgeX => IsSidebarMode && SidebarVisible ? (currentOffset < -90000.0 ? (SidebarVisible ? SidebarWidth : 0) : (currentOffset + SidebarWidth)) : -1;
+
+ /// Tests whether the given point is within the sidebar resize grip
+ private bool IsOnResizeEdge(int x, int y)
+ {
+ if (!IsSidebarMode || !SidebarVisible) return false;
+ double edge = SidebarEdgeX;
+ if (edge < 0) return false;
+ return x >= edge - SidebarResizeZone && x <= edge + SidebarResizeZone && y >= 0 && y <= Renderer.Screen.Height;
+ }
+
+ /// Processes a mouse up event
+
+ /// The current fade amount of the sidebar (0 = fully hidden, 1 = fully shown)
+ public double SidebarFade = 0.0;
+
+ public void UpdateTransition(double TimeElapsed)
+ {
+ if (!IsSidebarMode) return;
+
+ double target = SidebarVisible ? 0 : -SidebarWidth;
+ if (currentOffset < -90000.0)
+ {
+ currentOffset = target;
+ startOffset = target;
+ isAnimating = false;
+ }
+
+ if (isAnimating)
+ {
+ animationElapsed += TimeElapsed;
+ double x = System.Math.Min(animationElapsed / animationDuration, 1.0);
+ double progress = 1.0 - System.Math.Pow(1.0 - x, 3.0); // easeOutCubic
+ currentOffset = startOffset + (target - startOffset) * progress;
+ // Fade follows the same eased progress so the colour transition is smooth
+ SidebarFade = SidebarVisible ? progress : 1.0 - progress;
+
+ if (x >= 1.0)
+ {
+ currentOffset = target;
+ SidebarFade = SidebarVisible ? 1.0 : 0.0;
+ isAnimating = false;
+ if (!SidebarVisible)
+ {
+ Reset();
+ }
+ }
+ }
+ else
+ {
+ currentOffset = target;
+ SidebarFade = SidebarVisible ? 1.0 : 0.0;
+ }
+
+ menuMin.X = currentOffset;
+ menuMax.X = menuMin.X + SidebarWidth;
+
+ RepositionSidebarControls();
+ }
+
+ public virtual void RepositionSidebarControls()
+ {
+ if (OptionsTabContainer != null)
+ {
+ OptionsTabContainer.Location = new Vector2((float)menuMin.X + 16f, 50f);
+ float containerWidth = (float)SidebarWidth - 32f;
+ OptionsTabContainer.Size = new Vector2(containerWidth, (float)Renderer.Screen.Height - 100f);
+
+ foreach (var tab in OptionsTabContainer.TabContents)
+ {
+ if (tab is GLContainer container)
+ {
+ container.Size = new Vector2(containerWidth, container.Size.Y);
+ foreach (var child in container.Children)
+ {
+ if (child is GLHBoxContainer row)
+ {
+ // Row height follows the current UI font so labels/controls never clip
+ float rowHeight = Renderer.Fonts.NormalFont.FontSize + 10f;
+ row.Size = new Vector2(containerWidth, rowHeight);
+ if (row.Children.Count >= 2)
+ {
+ var label = row.Children[0] as Label;
+ var control = row.Children[1];
+ if (label != null) label.Size = new Vector2(label.Size.X, rowHeight);
+ control.Size = new Vector2(control.Size.X, rowHeight);
+ // Use the label's cached natural width so we don't re-measure every frame
+ float labelWidth = label != null ? (float)label.Size.X : (float)row.Children[0].Size.X;
+ float controlWidth = (float)(containerWidth - labelWidth - row.Spacing);
+ if (controlWidth < 60f)
+ {
+ // Not enough room: shrink the label so the control stays usable
+ labelWidth = containerWidth - 60f - row.Spacing;
+ controlWidth = 60f;
+ }
+ label.Size = new Vector2(labelWidth, row.Size.Y);
+ control.Size = new Vector2(controlWidth, row.Size.Y);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
/// Processes a mouse move event
/// The screen-relative x coordinate of the move event
/// The screen-relative y coordinate of the move event
@@ -178,11 +586,74 @@ public virtual bool ProcessMouseMove(int x, int y)
return true;
}
+ /// Toggles the sidebar open/closed, mirroring the on-screen toggle button
+ public void ToggleSidebar()
+ {
+ if (!IsSidebarMode) return;
+ SidebarVisible = !SidebarVisible;
+ startOffset = currentOffset;
+ animationElapsed = 0.0;
+ isAnimating = true;
+ if (SidebarVisible)
+ {
+ if (CurrMenu == -1)
+ {
+ PushMenu(MenuType.Options);
+ }
+ else
+ {
+ Renderer.CurrentInterface = InterfaceType.Menu;
+ }
+ }
+ else
+ {
+ Renderer.CurrentInterface = InterfaceType.Normal;
+ }
+ ComputePosition();
+ }
+
/// Processes a mouse down event
/// The screen-relative x coordinate of the down event
/// The screen-relative y coordinate of the down event
public void ProcessMouseDown(int x, int y)
{
+ if (IsSidebarMode)
+ {
+ if (IsOnResizeEdge(x, y))
+ {
+ isResizingSidebar = true;
+ return;
+ }
+ Vector4 rect = GetToggleButtonRect();
+ if (x >= rect.X && x <= rect.X + rect.Z && y >= rect.Y && y <= rect.Y + rect.W)
+ {
+ ToggleSidebar();
+ return;
+ }
+ }
+ if (CurrMenu >= 0 && Menus.Length > 0 && Menus[CurrMenu].Type == MenuType.Options)
+ {
+ if (OptionsTabContainer != null && OptionsTabContainer.IsVisible)
+ {
+ OptionsTabContainer.MouseDown(x, y);
+ return;
+ }
+ }
+ if (CurrMenu >= 0 && Menus.Length > 0)
+ {
+ var menu = Menus[CurrMenu];
+ if (menu.Selection >= 0 && menu.Selection < menu.Items.Length)
+ {
+ var entry = menu.Items[menu.Selection];
+ if (entry is MenuOption opt && (opt.Type == OptionType.ViewingDistance || opt.Type == OptionType.NearClip))
+ {
+ isScrubbing = true;
+ scrubbingOption = opt;
+ lastMouseX = x;
+ }
+ }
+ }
+
for (int i = 0; i < menuControls.Count; i++)
{
if (menuControls[i].IsVisible)
@@ -202,7 +673,73 @@ public void ProcessMouseDown(int x, int y)
ProcessCommand(Translations.Command.MenuUp, 0);
return;
}
- ProcessCommand(Translations.Command.MenuEnter, 0);
+ // If scrubbing, do not trigger entry actions
+ if (!isScrubbing)
+ {
+ ProcessCommand(Translations.Command.MenuEnter, 0);
+ }
+ }
+ }
+
+ /// Processes a mouse up event
+ /// Handles hover highlighting and edge-drag resizing of the sidebar. Returns true if the event was consumed.
+ /// The screen-relative x coordinate
+ /// The screen-relative y coordinate
+ public bool HandleSidebarResizeMove(int x, int y)
+ {
+ if (!IsSidebarMode || !SidebarVisible)
+ {
+ sidebarResizeHover = 0f;
+ return false;
+ }
+ if (isResizingSidebar)
+ {
+ double ratio = (double)x / Renderer.Screen.Width;
+ SidebarWidthRatio = System.Math.Max(0.15, System.Math.Min(ratio, 0.5));
+ ComputePosition();
+ return true;
+ }
+ bool onEdge = IsOnResizeEdge(x, y);
+ sidebarResizeHover = onEdge ? 1f : 0f;
+ return false;
+ }
+
+ /// Draws the sidebar resize grip / highlight on the right edge
+ public void DrawSidebarResizeGrip()
+ {
+ if (!IsSidebarMode || !SidebarVisible) return;
+ double edge = Renderer.Screen.Width * SidebarWidthRatio;
+ float fade = (float)System.Math.Max(SidebarFade, 0.001);
+ float baseAlpha = 0.5f * fade;
+ float alpha = (0.5f + 0.5f * sidebarResizeHover) * fade;
+ // Always-visible grip line so the user knows the edge is draggable
+ Renderer.Rectangle.Draw(null, new Vector2((float)(edge - 1), 0f), new Vector2(2f, (float)Renderer.Screen.Height), new Color128(0.0f, 0.47f, 0.83f, baseAlpha));
+ if (sidebarResizeHover > 0f)
+ {
+ Renderer.Rectangle.Draw(null, new Vector2((float)(edge - 3), 0f), new Vector2(6f, (float)Renderer.Screen.Height), new Color128(0.0f, 0.47f, 0.83f, 0.4f * alpha));
+ Renderer.Rectangle.Draw(null, new Vector2((float)edge - 0.5f, 0f), new Vector2(1.5f, (float)Renderer.Screen.Height), new Color128(0.0f, 0.47f, 0.83f, alpha));
+ }
+ }
+
+ public void ProcessMouseUp(int x, int y)
+ {
+ isScrubbing = false;
+ scrubbingOption = null;
+ isResizingSidebar = false;
+ if (CurrMenu >= 0 && Menus.Length > 0 && Menus[CurrMenu].Type == MenuType.Options)
+ {
+ if (OptionsTabContainer != null && OptionsTabContainer.IsVisible)
+ {
+ OptionsTabContainer.MouseUp(x, y);
+ return;
+ }
+ }
+ for (int i = 0; i < menuControls.Count; i++)
+ {
+ if (menuControls[i].IsVisible)
+ {
+ menuControls[i].MouseUp(x, y);
+ }
}
}
@@ -220,7 +757,6 @@ public virtual void DragFile(object sender, OpenTK.Input.FileDropEventArgs e)
}
-
/// Computes the position in the screen of the current menu.
/// Also sets the menu size
public void ComputePosition()
@@ -229,6 +765,10 @@ public void ComputePosition()
return;
MenuBase menu = Menus[CurrMenu];
+ if (OptionsTabContainer != null)
+ {
+ OptionsTabContainer.IsVisible = (menu.Type == MenuType.Options);
+ }
for (int i = 0; i < menu.Items.Length; i++)
{
/*
@@ -241,44 +781,162 @@ public void ComputePosition()
}
}
- // HORIZONTAL PLACEMENT
- switch (menu.Align)
+ if (IsSidebarMode)
{
- case TextAlignment.TopLeft:
- // Left aligned
- menuMin.X = 0;
- break;
- default:
- // Centered in window
- menuMin.X = (Renderer.Screen.Width - menu.Width) / 2; // menu left edge (border excluded)
- break;
+ menu.Align = TextAlignment.TopLeft;
+ menu.Width = SidebarWidth - 2.0 * Border.X;
+ menu.ItemWidth = menu.Width;
+
+ menuMin.X = SidebarVisible ? 0 : -SidebarWidth;
+ menuMax.X = menuMin.X + SidebarWidth;
+ menuMin.Y = 0;
+ menuMax.Y = Renderer.Screen.Height;
+ topItemY = Border.Y;
+ visibleItems = (int)(Renderer.Screen.Height - Border.Y * 2) / LineHeight;
+
+ RepositionSidebarControls();
+ }
+ else
+ {
+ // HORIZONTAL PLACEMENT
+ switch (menu.Align)
+ {
+ case TextAlignment.TopLeft:
+ // Left aligned
+ menuMin.X = 0;
+ break;
+ default:
+ // Centered in window
+ menuMin.X = (Renderer.Screen.Width - menu.Width) / 2; // menu left edge (border excluded)
+ break;
+ }
+
+ menuMax.X = menuMin.X + menu.Width; // menu right edge (border excluded)
+ // VERTICAL PLACEMENT: centre the menu in the main window
+ menuMin.Y = (Renderer.Screen.Height - menu.Height) / 2; // menu top edge (border excluded)
+ menuMax.Y = menuMin.Y + menu.Height; // menu bottom edge (border excluded)
+ topItemY = menuMin.Y; // top edge of top item
+ // assume all items fit in the screen
+ visibleItems = menu.Items.Length;
+
+ // if there are more items than can fit in the screen height,
+ // (there should be at least room for the menu top border)
+ if (menuMin.Y < Border.Y)
+ {
+ // the number of lines which fit in the screen
+ int numOfLines = (int)(Renderer.Screen.Height - Border.Y * 2) / LineHeight;
+ visibleItems = numOfLines - 2; // at least an empty line at the top and at the bottom
+ // split the menu in chunks of 'visibleItems' items
+ // and display the chunk which contains the currently selected item
+ menu.TopItem = menu.Selection - (menu.Selection % visibleItems);
+ visibleItems = menu.Items.Length - menu.TopItem < visibleItems ? // in the last chunk,
+ menu.Items.Length - menu.TopItem : visibleItems; // display remaining items only
+ menuMin.Y = (Renderer.Screen.Height - numOfLines * LineHeight) / 2.0;
+ menuMax.Y = menuMin.Y + numOfLines * LineHeight;
+ // first menu item is drawn on second line (first line is empty
+ // on first screen and contains an ellipsis on following screens
+ topItemY = menuMin.Y + LineHeight;
+ }
+ }
+ }
+
+ public void DrawSidebarToggleButton(double RealTimeElapsed)
+ {
+ if (!IsSidebarMode) return;
+ Vector4 rect = GetToggleButtonRect();
+ Renderer.Rectangle.Draw(null, new Vector2(rect.X, rect.Y), new Vector2(rect.Z, rect.W), backgroundColor);
+ string arrow = SidebarVisible ? "<" : ">";
+ if (MenuFont == null)
+ {
+ MenuFont = Renderer.Fonts.NormalFont;
+ }
+ Renderer.OpenGlString.Draw(MenuFont, arrow, new Vector2(rect.X + 8, rect.Y + 10), TextAlignment.TopLeft, ColourNormal, false);
+ }
+
+ public MenuEntry[] CreateOptionsEntries(HostApplication host)
+ {
+ List items = new List();
+ items.Add(new MenuCaption(this, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "panel", "options" })));
+
+ items.Add(new MenuOption(this, OptionType.ScreenResolution, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "resolution" }), Renderer.Screen.AvailableResolutions.ToArray()));
+ items.Add(new MenuOption(this, OptionType.FullScreen, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "display_mode_fullscreen" }), new[] { "true", "false" }));
+
+ items.Add(new MenuOption(this, OptionType.Interpolation, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation" }), new[]
+ {
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearest"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinear"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearestmipmap"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinearmipmap"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_trilinearmipmap"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_anisotropic"})
+ }));
+
+ items.Add(new MenuOption(this, OptionType.AnisotropicLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation_anisotropic_level" }), new[] { "0", "2", "4", "8", "16" }));
+ items.Add(new MenuOption(this, OptionType.AntialiasingLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation_antialiasing_level" }), new[] { "0", "2", "4", "8", "16" }));
+
+ items.Add(new MenuOption(this, OptionType.TransparencyQuality, "Transparency Quality", new[] { "Sharp", "Intermediate", "Smooth" }));
+
+ items.Add(new MenuOption(this, OptionType.ViewingDistance, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_distance_viewingdistance" }), new[] { "400", "600", "800", "1000", "1500", "2000" }));
+ items.Add(new MenuOption(this, OptionType.NearClip, "Near clip", new[] { "0.001", "0.01", "0.05", "0.1", "0.2", "0.5" }));
+
+ if (host == HostApplication.OpenBve)
+ {
+ items.Add(new MenuOption(this, OptionType.UIScaleFactor, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "ui_scalefactor" }), new[] { "1x", "2x", "3x", "4x", "5x", "6x" }));
+ items.Add(new MenuOption(this, OptionType.NumberOfSounds, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "misc_sound_number" }), new[] { "16", "32", "64", "128"}));
+ items.Add(new MenuOption(this, OptionType.ShadowQuality, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution" }), new[]
+ {
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","shadows_resolution_off"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","shadows_resolution_low"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","shadows_resolution_medium"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","shadows_resolution_high"}),
+ Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","shadows_resolution_ultra"})
+ }));
+ items.Add(new MenuOption(this, OptionType.ShadowFilterCascades, "Per-cascade culling", new[] { "true", "false" }));
}
- menuMax.X = menuMin.X + menu.Width; // menu right edge (border excluded)
- // VERTICAL PLACEMENT: centre the menu in the main window
- menuMin.Y = (Renderer.Screen.Height - menu.Height) / 2; // menu top edge (border excluded)
- menuMax.Y = menuMin.Y + menu.Height; // menu bottom edge (border excluded)
- topItemY = menuMin.Y; // top edge of top item
- // assume all items fit in the screen
- visibleItems = menu.Items.Length;
-
- // if there are more items than can fit in the screen height,
- // (there should be at least room for the menu top border)
- if (menuMin.Y < Border.Y)
- {
- // the number of lines which fit in the screen
- int numOfLines = (int)(Renderer.Screen.Height - Border.Y * 2) / LineHeight;
- visibleItems = numOfLines - 2; // at least an empty line at the top and at the bottom
- // split the menu in chunks of 'visibleItems' items
- // and display the chunk which contains the currently selected item
- menu.TopItem = menu.Selection - (menu.Selection % visibleItems);
- visibleItems = menu.Items.Length - menu.TopItem < visibleItems ? // in the last chunk,
- menu.Items.Length - menu.TopItem : visibleItems; // display remaining items only
- menuMin.Y = (Renderer.Screen.Height - numOfLines * LineHeight) / 2.0;
- menuMax.Y = menuMin.Y + numOfLines * LineHeight;
- // first menu item is drawn on second line (first line is empty
- // on first screen and contains an ellipsis on following screens
- topItemY = menuMin.Y + LineHeight;
+ items.Add(new MenuOption(this, OptionType.NewXParser, "Use New X Parser", new[] { "Original", "NewXParser", "Assimp" }));
+ items.Add(new MenuOption(this, OptionType.NewObjParser, "Use New Obj Parser", new[] { "Original", "Assimp" }));
+
+ if (host == HostApplication.ObjectViewer)
+ {
+ items.Add(new MenuOption(this, OptionType.AutoReloadObjects, "Automatically Reload Objects", new[] { "true", "false" }));
+ }
+
+ if (host == HostApplication.RouteViewer)
+ {
+ items.Add(new MenuOption(this, OptionType.ShowLogo, "Show Logo", new[] { "true", "false" }));
+ items.Add(new MenuOption(this, OptionType.ShowBackgrounds, "Show Backgrounds", new[] { "true", "false" }));
+ items.Add(new MenuOption(this, OptionType.ShowProgressBar, "Show Progress Bar", new[] { "true", "false" }));
+ }
+
+ items.Add(new MenuCommand(this, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "menu", "back" }), MenuTag.MenuBack, 0));
+ return items.ToArray();
+ }
+
+ /// Called when a menu option changes
+ /// The option type that changed
+ public virtual void OnOptionChanged(OptionType type)
+ {
+ }
+
+ /// Processes raw key down events for numeric option entry
+ /// The keyboard key pressed
+ public virtual void ProcessKeyDown(OpenTK.Input.Key key)
+ {
+ if (GLControl.FocusedControl != null && GLControl.FocusedControl.IsVisible)
+ {
+ GLControl.FocusedControl.KeyDown(key);
+ return;
+ }
+ if (CurrMenu < 0 || Menus.Length == 0) return;
+ var menu = Menus[CurrMenu];
+ if (menu.Selection >= 0 && menu.Selection < menu.Items.Length)
+ {
+ var selectedEntry = menu.Items[menu.Selection];
+ if (selectedEntry is MenuOption opt)
+ {
+ opt.ProcessKeyDown(key);
+ }
}
}
@@ -286,6 +944,5 @@ public void ComputePosition()
/// The real time elapsed since the last draw call
/// Note that the real time elapsed may be different to the game time elapsed
public abstract void Draw(double RealTimeElapsed);
-
}
}
diff --git a/source/LibRender2/Menu/Menu.OptionType.cs b/source/LibRender2/Menu/Menu.OptionType.cs
index 73d2beb99..fbdc98baa 100644
--- a/source/LibRender2/Menu/Menu.OptionType.cs
+++ b/source/LibRender2/Menu/Menu.OptionType.cs
@@ -48,6 +48,20 @@ public enum OptionType
/// Sets the shadow quality
ShadowQuality,
/// Sets whether shadow casters are filtered per cascade
- ShadowFilterCascades
+ ShadowFilterCascades,
+ /// Sets the transparency quality
+ TransparencyQuality,
+ /// Sets the X object parser
+ NewXParser,
+ /// Sets the OBJ object parser
+ NewObjParser,
+ /// Sets the near clipping plane distance
+ NearClip,
+ /// Sets whether to show the logo during loading
+ ShowLogo,
+ /// Sets whether to show the background during loading
+ ShowBackgrounds,
+ /// Sets whether to show the progress bar during loading
+ ShowProgressBar
}
}
diff --git a/source/LibRender2/Menu/MenuEntries/MenuOption.cs b/source/LibRender2/Menu/MenuEntries/MenuOption.cs
index c4330b989..1b16fa0d7 100644
--- a/source/LibRender2/Menu/MenuEntries/MenuOption.cs
+++ b/source/LibRender2/Menu/MenuEntries/MenuOption.cs
@@ -23,26 +23,46 @@
//SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
using LibRender2.Screens;
+using LibRender2.Viewports;
using OpenTK;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using OpenBveApi.Graphics;
using OpenBveApi.Interface;
+using OpenBveApi.Objects;
namespace LibRender2.Menu
{
public class MenuOption : MenuEntry
{
- private readonly OptionType Type;
+ internal readonly OptionType Type;
/// Holds the entries for all options
private readonly object[] Entries;
/// Gets the current option
- public object CurrentOption => Entries[CurrentlySelectedOption];
+ public object CurrentOption => customValue ?? Entries[CurrentlySelectedOption];
private int CurrentlySelectedOption;
+ private string customValue = null;
+
+ public string DisplayValue
+ {
+ get
+ {
+ string val = CurrentOption.ToString();
+ if ((Type == OptionType.ViewingDistance || Type == OptionType.NearClip) &&
+ BaseMenu.CurrMenu >= 0 && BaseMenu.CurrMenu < BaseMenu.Menus.Length &&
+ BaseMenu.Menus[BaseMenu.CurrMenu].Selection >= 0 &&
+ BaseMenu.Menus[BaseMenu.CurrMenu].Selection < BaseMenu.Menus[BaseMenu.CurrMenu].Items.Length &&
+ BaseMenu.Menus[BaseMenu.CurrMenu].Items[BaseMenu.Menus[BaseMenu.CurrMenu].Selection] == this)
+ {
+ return val + "_";
+ }
+ return val;
+ }
+ }
public MenuOption(AbstractMenu menu, OptionType type, string text, object[] entries) : base(menu)
{
@@ -102,6 +122,7 @@ public MenuOption(AbstractMenu menu, OptionType type, string text, object[] entr
}
break;
case OptionType.ViewingDistance:
+ customValue = BaseMenu.CurrentOptions.ViewingDistance.ToString();
switch (BaseMenu.CurrentOptions.ViewingDistance)
{
case 400:
@@ -172,6 +193,36 @@ public MenuOption(AbstractMenu menu, OptionType type, string text, object[] entr
case OptionType.ShadowFilterCascades:
CurrentlySelectedOption = BaseMenu.CurrentOptions.ShadowFilterCascades ? 0 : 1;
return;
+ case OptionType.TransparencyQuality:
+ CurrentlySelectedOption = (int)BaseMenu.CurrentOptions.TransparencyMode;
+ return;
+ case OptionType.NewXParser:
+ CurrentlySelectedOption = (int)BaseMenu.CurrentOptions.CurrentXParser;
+ return;
+ case OptionType.NewObjParser:
+ CurrentlySelectedOption = (int)BaseMenu.CurrentOptions.CurrentObjParser;
+ return;
+ case OptionType.NearClip:
+ customValue = BaseMenu.CurrentOptions.NearClipBase.ToString(CultureInfo.InvariantCulture);
+ for (int i = 0; i < Entries.Length; i++)
+ {
+ double clip = double.Parse(entries[i] as string ?? string.Empty, NumberStyles.Float, CultureInfo.InvariantCulture);
+ if (System.Math.Abs(clip - BaseMenu.CurrentOptions.NearClipBase) < 0.001)
+ {
+ CurrentlySelectedOption = i;
+ return;
+ }
+ }
+ break;
+ case OptionType.ShowLogo:
+ CurrentlySelectedOption = BaseMenu.CurrentOptions.LoadingLogo ? 0 : 1;
+ return;
+ case OptionType.ShowBackgrounds:
+ CurrentlySelectedOption = BaseMenu.CurrentOptions.LoadingBackground ? 0 : 1;
+ return;
+ case OptionType.ShowProgressBar:
+ CurrentlySelectedOption = BaseMenu.CurrentOptions.LoadingProgressBar ? 0 : 1;
+ return;
}
CurrentlySelectedOption = 0;
}
@@ -179,6 +230,7 @@ public MenuOption(AbstractMenu menu, OptionType type, string text, object[] entr
/// Flips to the next option
public void Flip()
{
+ customValue = null;
if (CurrentlySelectedOption < Entries.Length - 1)
{
CurrentlySelectedOption++;
@@ -267,6 +319,8 @@ public void Flip()
break;
case OptionType.Interpolation:
BaseMenu.CurrentOptions.Interpolation = (InterpolationMode)CurrentlySelectedOption;
+ BaseMenu.Renderer.TextureManager.UnloadAllTextures(true);
+ BaseMenu.Renderer.TextureManager.LoadAllTextures();
break;
case OptionType.AutoReloadObjects:
BaseMenu.CurrentOptions.AutoReloadObjects = !BaseMenu.CurrentOptions.AutoReloadObjects;
@@ -274,12 +328,20 @@ public void Flip()
//HACK: We can't store plain ints due to to boxing, so store strings and parse instead
case OptionType.AnisotropicLevel:
BaseMenu.CurrentOptions.AnisotropicFilteringLevel = int.Parse((string)CurrentOption, NumberStyles.Integer);
+ BaseMenu.Renderer.TextureManager.UnloadAllTextures(true);
+ BaseMenu.Renderer.TextureManager.LoadAllTextures();
break;
case OptionType.AntialiasingLevel:
BaseMenu.CurrentOptions.AntiAliasingLevel = int.Parse((string)CurrentOption, NumberStyles.Integer);
break;
case OptionType.ViewingDistance:
BaseMenu.CurrentOptions.ViewingDistance = int.Parse((string)CurrentOption, NumberStyles.Integer);
+ BaseMenu.Renderer.UpdateViewport(ViewportChangeMode.ChangeToScenery);
+ if (BaseMenu.Renderer.CameraTrackFollower != null)
+ {
+ BaseMenu.Renderer.UpdateViewingDistances(BaseMenu.CurrentOptions.ViewingDistance);
+ }
+ BaseMenu.OnOptionChanged(Type);
break;
case OptionType.UIScaleFactor:
string currentOption = (string)CurrentOption;
@@ -322,9 +384,148 @@ public void Flip()
case OptionType.ShadowFilterCascades:
BaseMenu.CurrentOptions.ShadowFilterCascades = !BaseMenu.CurrentOptions.ShadowFilterCascades;
break;
+ case OptionType.TransparencyQuality:
+ BaseMenu.CurrentOptions.TransparencyMode = (TransparencyMode)CurrentlySelectedOption;
+ break;
+ case OptionType.NewXParser:
+ BaseMenu.CurrentOptions.CurrentXParser = (XParsers)CurrentlySelectedOption;
+ break;
+ case OptionType.NewObjParser:
+ BaseMenu.CurrentOptions.CurrentObjParser = (ObjParsers)CurrentlySelectedOption;
+ break;
+ case OptionType.NearClip:
+ BaseMenu.CurrentOptions.NearClipBase = double.Parse((string)CurrentOption, NumberStyles.Float, CultureInfo.InvariantCulture);
+ BaseMenu.Renderer.UpdateViewport(ViewportChangeMode.ChangeToScenery);
+ BaseMenu.OnOptionChanged(Type);
+ break;
+ case OptionType.ShowLogo:
+ BaseMenu.CurrentOptions.LoadingLogo = !BaseMenu.CurrentOptions.LoadingLogo;
+ break;
+ case OptionType.ShowBackgrounds:
+ BaseMenu.CurrentOptions.LoadingBackground = !BaseMenu.CurrentOptions.LoadingBackground;
+ break;
+ case OptionType.ShowProgressBar:
+ BaseMenu.CurrentOptions.LoadingProgressBar = !BaseMenu.CurrentOptions.LoadingProgressBar;
+ break;
+
+ }
+ }
+
+ public void ProcessKeyDown(OpenTK.Input.Key key)
+ {
+ if (Type != OptionType.ViewingDistance && Type != OptionType.NearClip)
+ {
+ return;
}
+ if (customValue == null)
+ {
+ customValue = Entries[CurrentlySelectedOption].ToString();
+ }
+
+ if (key >= OpenTK.Input.Key.Number0 && key <= OpenTK.Input.Key.Number9)
+ {
+ int digit = (int)key - (int)OpenTK.Input.Key.Number0;
+ if (customValue == Entries[CurrentlySelectedOption].ToString())
+ {
+ customValue = digit.ToString();
+ }
+ else
+ {
+ customValue += digit.ToString();
+ }
+ ApplyCustomValue();
+ }
+ else if (key >= OpenTK.Input.Key.Keypad0 && key <= OpenTK.Input.Key.Keypad9)
+ {
+ int digit = (int)key - (int)OpenTK.Input.Key.Keypad0;
+ if (customValue == Entries[CurrentlySelectedOption].ToString())
+ {
+ customValue = digit.ToString();
+ }
+ else
+ {
+ customValue += digit.ToString();
+ }
+ ApplyCustomValue();
+ }
+ else if (key == OpenTK.Input.Key.Period || key == OpenTK.Input.Key.KeypadPeriod)
+ {
+ if (!customValue.Contains("."))
+ {
+ customValue += ".";
+ ApplyCustomValue();
+ }
+ }
+ else if (key == OpenTK.Input.Key.BackSpace)
+ {
+ if (customValue.Length > 0)
+ {
+ customValue = customValue.Substring(0, customValue.Length - 1);
+ ApplyCustomValue();
+ }
+ }
+ }
+
+ private void ApplyCustomValue()
+ {
+ if (string.IsNullOrEmpty(customValue))
+ {
+ return;
+ }
+
+ if (Type == OptionType.ViewingDistance)
+ {
+ if (double.TryParse(customValue, NumberStyles.Float, CultureInfo.InvariantCulture, out double val))
+ {
+ BaseMenu.CurrentOptions.ViewingDistance = (int)val;
+ BaseMenu.Renderer.UpdateViewport(ViewportChangeMode.ChangeToScenery);
+ if (BaseMenu.Renderer.CameraTrackFollower != null)
+ {
+ BaseMenu.Renderer.UpdateViewingDistances(BaseMenu.CurrentOptions.ViewingDistance);
+ }
+ BaseMenu.OnOptionChanged(Type);
+ }
+ }
+ else if (Type == OptionType.NearClip)
+ {
+ if (double.TryParse(customValue, NumberStyles.Float, CultureInfo.InvariantCulture, out double val))
+ {
+ BaseMenu.CurrentOptions.NearClipBase = val;
+ BaseMenu.Renderer.UpdateViewport(ViewportChangeMode.ChangeToScenery);
+ BaseMenu.OnOptionChanged(Type);
+ }
+ }
+ }
+
+ public void ApplyScrubDelta(int deltaX)
+ {
+ if (customValue == null)
+ {
+ customValue = Entries[CurrentlySelectedOption].ToString();
+ }
+
+ if (Type == OptionType.ViewingDistance)
+ {
+ if (double.TryParse(customValue, NumberStyles.Float, CultureInfo.InvariantCulture, out double val))
+ {
+ val += deltaX * 2;
+ if (val < 10) val = 10;
+ customValue = ((int)val).ToString();
+ ApplyCustomValue();
+ }
+ }
+ else if (Type == OptionType.NearClip)
+ {
+ if (double.TryParse(customValue, NumberStyles.Float, CultureInfo.InvariantCulture, out double val))
+ {
+ val += deltaX * 0.002;
+ if (val < 0.001) val = 0.001;
+ customValue = val.ToString("0.000", CultureInfo.InvariantCulture);
+ ApplyCustomValue();
+ }
+ }
}
}
}
diff --git a/source/LibRender2/Primitives/GLBoxContainer.cs b/source/LibRender2/Primitives/GLBoxContainer.cs
new file mode 100644
index 000000000..8393a6b0c
--- /dev/null
+++ b/source/LibRender2/Primitives/GLBoxContainer.cs
@@ -0,0 +1,61 @@
+using OpenBveApi.Math;
+
+namespace LibRender2.Primitives
+{
+ public enum BoxDirection
+ {
+ Horizontal,
+ Vertical
+ }
+
+ public class GLBoxContainer : GLContainer
+ {
+ public BoxDirection Direction = BoxDirection.Vertical;
+ public float Spacing = 4f;
+ /// Padding applied to all sides of the container before laying out children
+ public float Padding = 0f;
+
+ public GLBoxContainer(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ // Update child positions based on direction, spacing and padding
+ double offset = Padding;
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (!Children[i].IsVisible) continue;
+ if (Direction == BoxDirection.Vertical)
+ {
+ Children[i].Location = new Vector2(Location.X + Padding, Location.Y + offset);
+ offset += Children[i].Size.Y + Spacing;
+ }
+ else
+ {
+ Children[i].Location = new Vector2(Location.X + offset, Location.Y + Padding);
+ offset += Children[i].Size.X + Spacing;
+ }
+ }
+ base.Draw();
+ }
+ }
+
+ public class GLHBoxContainer : GLBoxContainer
+ {
+ public GLHBoxContainer(BaseRenderer renderer) : base(renderer)
+ {
+ Direction = BoxDirection.Horizontal;
+ }
+ }
+
+ public class GLVBoxContainer : GLBoxContainer
+ {
+ public GLVBoxContainer(BaseRenderer renderer) : base(renderer)
+ {
+ Direction = BoxDirection.Vertical;
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLButton.cs b/source/LibRender2/Primitives/GLButton.cs
new file mode 100644
index 000000000..0bf03bfa7
--- /dev/null
+++ b/source/LibRender2/Primitives/GLButton.cs
@@ -0,0 +1,103 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using System;
+using OpenBveApi.Graphics;
+
+namespace LibRender2.Primitives
+{
+ /// A modern styled button widget for the GL control system
+ public class GLButton : GLControl
+ {
+ public string Text = "";
+ public EventHandler OnClick;
+
+ /// The normal background color
+ public Color128 NormalColor = new Color128(0.0f, 0.47f, 0.83f, 1.0f);
+ /// The hover background color
+ public Color128 HoverColor = new Color128(0.1f, 0.55f, 0.93f, 1.0f);
+ /// The pressed background color
+ public Color128 PressColor = new Color128(0.0f, 0.37f, 0.65f, 1.0f);
+ /// The disabled background color
+ public Color128 DisabledColor = new Color128(0.2f, 0.2f, 0.2f, 1.0f);
+ /// Whether the button is enabled
+ public bool Enabled = true;
+ /// Whether this is a flat/minimal style button
+ public bool Flat = false;
+ /// Border radius in pixels (0 = sharp corners)
+ public float CornerRadius = 3f;
+
+ public GLButton(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ Size = new Vector2(100, 28);
+ BackgroundColor = NormalColor;
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+
+ if (!Enabled)
+ {
+ Renderer.Rectangle.Draw(null, Location, Size, DisabledColor);
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Text,
+ new Vector2(Location.X + (Size.X - Renderer.Fonts.NormalFont.MeasureString(Text).X) / 2.0, Location.Y + 4),
+ TextAlignment.TopLeft, new Color128(0.5f, 0.5f, 0.5f, 1f));
+ return;
+ }
+
+ Color128 bgColor;
+ if (IsPressed && CurrentlySelected)
+ {
+ bgColor = PressColor;
+ }
+ else if (CurrentlySelected)
+ {
+ bgColor = HoverColor;
+ }
+ else
+ {
+ bgColor = Flat ? BackgroundColor : NormalColor;
+ }
+
+ // Draw background
+ Renderer.Rectangle.Draw(null, Location, Size, bgColor);
+
+ // Draw subtle top highlight line (gives 3D feel)
+ if (!Flat)
+ {
+ Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X, 1.0), new Color128(1f, 1f, 1f, 0.15f));
+ }
+
+ // Draw text centered
+ Vector2 textSize = Renderer.Fonts.NormalFont.MeasureString(Text);
+ Vector2 textPos = new Vector2(Location.X + (Size.X - textSize.X) / 2.0, Location.Y + (Size.Y - textSize.Y) / 2.0);
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Text, textPos, TextAlignment.TopLeft, Color128.White);
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible) return;
+ CurrentlySelected = Enabled && (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y);
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible || !Enabled) return;
+ if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ IsPressed = true;
+ }
+ }
+
+ public override void MouseUp(int x, int y)
+ {
+ if (!IsVisible || !Enabled) return;
+ if (IsPressed && x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ IsPressed = false;
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLContainer.cs b/source/LibRender2/Primitives/GLContainer.cs
new file mode 100644
index 000000000..8547f79c7
--- /dev/null
+++ b/source/LibRender2/Primitives/GLContainer.cs
@@ -0,0 +1,85 @@
+using System.Collections.Generic;
+using OpenBveApi.Math;
+
+namespace LibRender2.Primitives
+{
+ public abstract class GLContainer : GLControl
+ {
+ public readonly List Children = new List();
+
+ protected GLContainer(BaseRenderer renderer) : base(renderer)
+ {
+ }
+
+ /// Whether this container should clip child rendering to its own bounds
+ public bool ClipChildren = false;
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (Children[i].IsVisible && Children[i].ClipToParent && ClipChildren)
+ {
+ // Simple clipping check: skip drawing if child is entirely outside container bounds
+ if (Children[i].Location.X + Children[i].Size.X < Location.X ||
+ Children[i].Location.X > Location.X + Size.X ||
+ Children[i].Location.Y + Children[i].Size.Y < Location.Y ||
+ Children[i].Location.Y > Location.Y + Size.Y)
+ {
+ continue;
+ }
+ }
+ Children[i].Draw();
+ }
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible) return;
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (Children[i].IsVisible)
+ {
+ Children[i].MouseMove(x, y);
+ }
+ }
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible) return;
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (Children[i].IsVisible)
+ {
+ Children[i].MouseDown(x, y);
+ }
+ }
+ }
+
+ public override void MouseUp(int x, int y)
+ {
+ if (!IsVisible) return;
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (Children[i].IsVisible)
+ {
+ Children[i].MouseUp(x, y);
+ }
+ }
+ }
+
+ public override void MouseWheel(int delta)
+ {
+ if (!IsVisible) return;
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (Children[i].IsVisible)
+ {
+ Children[i].MouseWheel(delta);
+ }
+ }
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLControl.cs b/source/LibRender2/Primitives/GLControl.cs
index 43ff49f02..530e4ce06 100644
--- a/source/LibRender2/Primitives/GLControl.cs
+++ b/source/LibRender2/Primitives/GLControl.cs
@@ -1,4 +1,4 @@
-//Simplified BSD License (BSD-2-Clause)
+//Simplified BSD License (BSD-2-Clause)
//
//Copyright (c) 2023, Christopher Lees, The OpenBVE Project
//
@@ -29,48 +29,74 @@
namespace LibRender2.Primitives
{
- /// An abstract OpenGL based control
- public abstract class GLControl
- {
- /// Holds a reference to the base renderer
- internal readonly BaseRenderer Renderer;
- /// The background color for the control
- public Color128 BackgroundColor;
- /// The texture for the control
- public Texture Texture;
- /// The stored location for the control
- public Vector2 Location;
- /// The stored size for the control
- public Vector2 Size;
- /// Whether the control is currently selected by the mouse
- public bool CurrentlySelected;
- /// The event handler for the OnClick event
- public EventHandler OnClick;
- /// Whether the control is currently visible
- public bool IsVisible;
+ /// An abstract OpenGL based control
+ public abstract class GLControl
+ {
+ /// Holds a reference to the base renderer
+ internal readonly BaseRenderer Renderer;
+ /// The background color for the control
+ public Color128 BackgroundColor;
+ /// The texture for the control
+ public Texture Texture;
+ /// The stored location for the control
+ public Vector2 Location;
+ /// The stored size for the control
+ public Vector2 Size;
+ /// Whether the control is currently selected by the mouse
+ public bool CurrentlySelected;
+ /// The event handler for the OnClick event
+ public EventHandler OnClick;
+ /// Whether the control is currently visible
+ public bool IsVisible;
+ /// Whether the control is currently being pressed
+ public bool IsPressed;
+ /// Whether the control supports clipping to its parent bounds
+ public bool ClipToParent = true;
+ /// The currently globally focused control for keyboard input
+ public static GLControl FocusedControl;
- protected GLControl(BaseRenderer renderer)
- {
- Renderer = renderer;
- }
+ protected GLControl(BaseRenderer renderer)
+ {
+ Renderer = renderer;
+ }
- /// Draws the control
- public abstract void Draw();
+ /// Draws the control
+ public abstract void Draw();
- /// Passes a mouse move event to the control
- /// The absolute screen X co-ordinate
- /// The absolute screen Y co-ordinate
- public virtual void MouseMove(int x, int y)
- {
+ /// Passes a keyboard key down event to the control
+ public virtual void KeyDown(OpenTK.Input.Key key)
+ {
+ }
- }
+ /// Passes a mouse move event to the control
+ /// The absolute screen X co-ordinate
+ /// The absolute screen Y co-ordinate
+ public virtual void MouseMove(int x, int y)
+ {
- /// Passes a mouse down event to the control
- /// The absolute screen X co-ordinate
- /// The absolute screen Y co-ordinate
- public virtual void MouseDown(int x, int y)
- {
- MouseMove(x, y);
- }
- }
+ }
+
+ /// Passes a mouse down event to the control
+ /// The absolute screen X co-ordinate
+ /// The absolute screen Y co-ordinate
+ public virtual void MouseDown(int x, int y)
+ {
+ MouseMove(x, y);
+ }
+
+ /// Passes a mouse up event to the control
+ /// The absolute screen X co-ordinate
+ /// The absolute screen Y co-ordinate
+ public virtual void MouseUp(int x, int y)
+ {
+ IsPressed = false;
+ }
+
+ /// Passes a mouse wheel event to the control
+ /// The scroll wheel delta
+ public virtual void MouseWheel(int delta)
+ {
+
+ }
+ }
}
diff --git a/source/LibRender2/Primitives/GLDropdown.cs b/source/LibRender2/Primitives/GLDropdown.cs
new file mode 100644
index 000000000..3bbbbb9e1
--- /dev/null
+++ b/source/LibRender2/Primitives/GLDropdown.cs
@@ -0,0 +1,184 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using System;
+using System.Collections.Generic;
+using OpenBveApi.Graphics;
+
+namespace LibRender2.Primitives
+{
+ public class GLDropdown : GLControl
+ {
+ public readonly List Items = new List();
+ public int SelectedIndex = 0;
+ public bool Expanded = false;
+ public EventHandler SelectionChanged;
+ private int hoveredIndex = -1;
+
+ public GLDropdown(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ Size = new Vector2(160, 24);
+ BackgroundColor = new Color128(0.15f, 0.15f, 0.15f, 1f);
+ }
+
+ /// Checks whether the dropdown should expand upward to avoid overflowing the screen
+ private bool ShouldExpandUpward()
+ {
+ float itemHeight = 24f;
+ float overlayHeight = Items.Count * itemHeight;
+ return (Location.Y + Size.Y + overlayHeight) > Renderer.Screen.Height;
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ // Draw main box
+ Color128 boxBg = Expanded ? new Color128(0.22f, 0.22f, 0.26f, 1f) : BackgroundColor;
+ Renderer.Rectangle.Draw(null, Location, Size, boxBg);
+ // Draw border
+ Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X, 1.0), new Color128(0.0f, 0.47f, 0.83f, 1f));
+ // Draw arrow indicator
+ float arrowX = (float)(Location.X + Size.X - 18);
+ float arrowY = (float)(Location.Y + (Size.Y - 5) / 2);
+ if (Expanded)
+ {
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 4, arrowY), new Vector2(1, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 3, arrowY + 1), new Vector2(3, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 2, arrowY + 2), new Vector2(5, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 1, arrowY + 3), new Vector2(7, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX, arrowY + 4), new Vector2(9, 1), Color128.White);
+ }
+ else
+ {
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX, arrowY), new Vector2(9, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 1, arrowY + 1), new Vector2(7, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 2, arrowY + 2), new Vector2(5, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 3, arrowY + 3), new Vector2(3, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(arrowX + 4, arrowY + 4), new Vector2(1, 1), Color128.White);
+ }
+
+ if (SelectedIndex >= 0 && SelectedIndex < Items.Count)
+ {
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Items[SelectedIndex], new Vector2(Location.X + 8, Location.Y + 2), TextAlignment.TopLeft, Color128.White);
+ }
+ }
+
+ public void DrawOverlay()
+ {
+ if (!IsVisible || !Expanded || Items.Count == 0) return;
+
+ float itemHeight = 24f;
+ float overlayHeight = Items.Count * itemHeight;
+ bool expandUp = ShouldExpandUpward();
+
+ Vector2 overlayLoc = expandUp
+ ? new Vector2(Location.X, Location.Y - overlayHeight)
+ : new Vector2(Location.X, Location.Y + Size.Y);
+
+ // Draw overlay background
+ Renderer.Rectangle.Draw(null, overlayLoc, new Vector2(Size.X, overlayHeight), new Color128(0.1f, 0.1f, 0.1f, 0.95f));
+ // Draw overlay border
+ Renderer.Rectangle.Draw(null, overlayLoc, new Vector2(Size.X, overlayHeight), new Color128(0.0f, 0.35f, 0.65f, 1f));
+
+ for (int i = 0; i < Items.Count; i++)
+ {
+ Vector2 itemLoc = expandUp
+ ? new Vector2(Location.X, Location.Y - overlayHeight + i * itemHeight)
+ : new Vector2(Location.X, Location.Y + Size.Y + i * itemHeight);
+ if (i == SelectedIndex)
+ {
+ Renderer.Rectangle.Draw(null, itemLoc, new Vector2(Size.X, itemHeight), Color128.Orange);
+ }
+ else if (i == hoveredIndex)
+ {
+ Renderer.Rectangle.Draw(null, itemLoc, new Vector2(Size.X, itemHeight), new Color128(0.2f, 0.25f, 0.35f, 1f));
+ }
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Items[i], new Vector2(itemLoc.X + 8, itemLoc.Y + 2), TextAlignment.TopLeft, Color128.White);
+ }
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible) return;
+ CurrentlySelected = (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y);
+
+ if (Expanded)
+ {
+ float itemHeight = 24f;
+ float overlayHeight = Items.Count * itemHeight;
+ bool expandUp = ShouldExpandUpward();
+ Vector2 overlayLoc = expandUp
+ ? new Vector2(Location.X, Location.Y - overlayHeight)
+ : new Vector2(Location.X, Location.Y + Size.Y);
+
+ if (x >= overlayLoc.X && x <= overlayLoc.X + Size.X && y >= overlayLoc.Y && y <= overlayLoc.Y + overlayHeight)
+ {
+ hoveredIndex = (int)((y - overlayLoc.Y) / itemHeight);
+ if (hoveredIndex < 0 || hoveredIndex >= Items.Count) hoveredIndex = -1;
+ }
+ else
+ {
+ hoveredIndex = -1;
+ }
+ }
+ else
+ {
+ hoveredIndex = -1;
+ }
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible) return;
+ if (Expanded)
+ {
+ float itemHeight = 24f;
+ float overlayHeight = Items.Count * itemHeight;
+ bool expandUp = ShouldExpandUpward();
+ Vector2 overlayLoc = expandUp
+ ? new Vector2(Location.X, Location.Y - overlayHeight)
+ : new Vector2(Location.X, Location.Y + Size.Y);
+
+ if (x >= overlayLoc.X && x <= overlayLoc.X + Size.X && y >= overlayLoc.Y && y <= overlayLoc.Y + overlayHeight)
+ {
+ int item = (int)((y - overlayLoc.Y) / itemHeight);
+ if (item >= 0 && item < Items.Count)
+ {
+ SelectedIndex = item;
+ SelectionChanged?.Invoke(this, EventArgs.Empty);
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ Expanded = false;
+ if (ActiveDropdown == this) ActiveDropdown = null;
+ hoveredIndex = -1;
+ return;
+ }
+ }
+
+ if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ Expanded = !Expanded;
+ if (Expanded)
+ {
+ if (ActiveDropdown != null && ActiveDropdown != this)
+ {
+ ActiveDropdown.Expanded = false;
+ }
+ ActiveDropdown = this;
+ }
+ else
+ {
+ if (ActiveDropdown == this) ActiveDropdown = null;
+ }
+ }
+ else
+ {
+ Expanded = false;
+ if (ActiveDropdown == this) ActiveDropdown = null;
+ hoveredIndex = -1;
+ }
+ }
+
+ public static GLDropdown ActiveDropdown;
+ }
+}
diff --git a/source/LibRender2/Primitives/GLLabel.cs b/source/LibRender2/Primitives/GLLabel.cs
new file mode 100644
index 000000000..3383ce06a
--- /dev/null
+++ b/source/LibRender2/Primitives/GLLabel.cs
@@ -0,0 +1,40 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using OpenBveApi.Graphics;
+
+namespace LibRender2.Primitives
+{
+ /// A text label widget for the GL control system
+ public class GLLabel : GLControl
+ {
+ public string Text = "";
+ public Color128 TextColor = Color128.White;
+ /// Text alignment within the label's bounds
+ public TextAlignment Alignment = TextAlignment.TopLeft;
+
+ public GLLabel(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ Size = new Vector2(100, 20);
+ BackgroundColor = new Color128(0, 0, 0, 0); // Transparent by default
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ // Draw background only if it has opacity
+ if (BackgroundColor.A > 0.01f)
+ {
+ Renderer.Rectangle.Draw(null, Location, Size, BackgroundColor);
+ }
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Text, Location, Alignment, TextColor);
+ }
+
+ /// Measures the text and updates Size to fit
+ public void AutoSize()
+ {
+ Vector2 textSize = Renderer.Fonts.NormalFont.MeasureString(Text);
+ Size = new Vector2((float)textSize.X + 4f, (float)textSize.Y + 2f);
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLLinkButton.cs b/source/LibRender2/Primitives/GLLinkButton.cs
new file mode 100644
index 000000000..5295c650c
--- /dev/null
+++ b/source/LibRender2/Primitives/GLLinkButton.cs
@@ -0,0 +1,64 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using System;
+using OpenBveApi.Graphics;
+
+namespace LibRender2.Primitives
+{
+ public class GLLinkButton : GLControl
+ {
+ public string Text = "";
+ public Color128 NormalColor = new Color128(0.2f, 0.6f, 1f, 1f);
+ public Color128 HoverColor = new Color128(0.4f, 0.8f, 1f, 1f);
+ public Color128 PressColor = new Color128(0.15f, 0.5f, 0.85f, 1f);
+ public EventHandler OnClick;
+
+ public GLLinkButton(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ Size = new Vector2(100, 20);
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ Color128 color = NormalColor;
+ if (IsPressed && CurrentlySelected)
+ {
+ color = PressColor;
+ }
+ else if (CurrentlySelected)
+ {
+ color = HoverColor;
+ }
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Text, Location, TextAlignment.TopLeft, color);
+ Vector2 textSize = Renderer.Fonts.NormalFont.MeasureString(Text);
+ Renderer.Rectangle.Draw(null, new Vector2(Location.X, Location.Y + textSize.Y), new Vector2(textSize.X, 1f), color);
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible) return;
+ CurrentlySelected = (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y);
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible) return;
+ if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ IsPressed = true;
+ }
+ }
+
+ public override void MouseUp(int x, int y)
+ {
+ if (!IsVisible) return;
+ if (IsPressed && x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ IsPressed = false;
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLNumericUpDown.cs b/source/LibRender2/Primitives/GLNumericUpDown.cs
new file mode 100644
index 000000000..10d090523
--- /dev/null
+++ b/source/LibRender2/Primitives/GLNumericUpDown.cs
@@ -0,0 +1,188 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using System;
+using OpenBveApi.Graphics;
+
+namespace LibRender2.Primitives
+{
+ public class GLNumericUpDown : GLControl
+ {
+ public float Value = 0f;
+ public float Minimum = 0f;
+ public float Maximum = 100f;
+ public float Increment = 1f;
+ public EventHandler ValueChanged;
+
+ private bool upPressed = false;
+ private bool downPressed = false;
+ public bool IsEditing = false;
+ private string currentInputString = "";
+
+ public GLNumericUpDown(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ Size = new Vector2(120, 24);
+ BackgroundColor = new Color128(0.15f, 0.15f, 0.15f, 1f);
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ Color128 fieldBg = IsPressed ? new Color128(0.22f, 0.22f, 0.26f, 1f) : BackgroundColor;
+ Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X - 34, Size.Y), fieldBg);
+
+ // Draw active/edit border
+ Color128 borderColor = IsEditing ? Color128.Orange : new Color128(0.0f, 0.47f, 0.83f, 1f);
+ Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X - 34, 1.0), borderColor);
+
+ string textToDraw = IsEditing ? currentInputString + "|" : Value.ToString("0.##");
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, textToDraw, new Vector2(Location.X + 8, Location.Y + 2), TextAlignment.TopLeft, Color128.White);
+
+ Vector2 upLoc = new Vector2(Location.X + Size.X - 32, Location.Y);
+ Vector2 downLoc = new Vector2(Location.X + Size.X - 16, Location.Y);
+
+ // Draw Up (+) button
+ Color128 upBg = upPressed ? Color128.Orange : new Color128(0.25f, 0.25f, 0.25f, 1f);
+ Renderer.Rectangle.Draw(null, upLoc, new Vector2(15, Size.Y), upBg);
+ float plusX = (float)(upLoc.X + (15 - 7) / 2);
+ float plusY = (float)(upLoc.Y + (Size.Y - 7) / 2);
+ Renderer.Rectangle.Draw(null, new Vector2(plusX, plusY + 3), new Vector2(7, 1), Color128.White);
+ Renderer.Rectangle.Draw(null, new Vector2(plusX + 3, plusY), new Vector2(1, 7), Color128.White);
+
+ // Draw Down (-) button
+ Color128 downBg = downPressed ? Color128.Orange : new Color128(0.25f, 0.25f, 0.25f, 1f);
+ Renderer.Rectangle.Draw(null, downLoc, new Vector2(15, Size.Y), downBg);
+ float minusX = (float)(downLoc.X + (15 - 7) / 2);
+ float minusY = (float)(downLoc.Y + (Size.Y - 1) / 2);
+ Renderer.Rectangle.Draw(null, new Vector2(minusX, minusY), new Vector2(7, 1), Color128.White);
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible) return;
+ CurrentlySelected = (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y);
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible) return;
+ Vector2 upLoc = new Vector2(Location.X + Size.X - 32, Location.Y);
+ Vector2 downLoc = new Vector2(Location.X + Size.X - 16, Location.Y);
+
+ if (x >= upLoc.X && x <= upLoc.X + 15 && y >= upLoc.Y && y <= upLoc.Y + Size.Y)
+ {
+ CommitEdit();
+ upPressed = true;
+ IsPressed = true;
+ Value = System.Math.Min(Maximum, Value + Increment);
+ ValueChanged?.Invoke(this, EventArgs.Empty);
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ else if (x >= downLoc.X && x <= downLoc.X + 15 && y >= downLoc.Y && y <= downLoc.Y + Size.Y)
+ {
+ CommitEdit();
+ downPressed = true;
+ IsPressed = true;
+ Value = System.Math.Max(Minimum, Value - Increment);
+ ValueChanged?.Invoke(this, EventArgs.Empty);
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ else if (x >= Location.X && x < Location.X + Size.X - 32 && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ if (!IsEditing)
+ {
+ if (FocusedControl != null && FocusedControl != this && FocusedControl is GLNumericUpDown otherNum)
+ {
+ otherNum.CommitEdit();
+ }
+ IsEditing = true;
+ currentInputString = Value.ToString("0.##");
+ FocusedControl = this;
+ }
+ }
+ else
+ {
+ CommitEdit();
+ }
+ }
+
+ public override void MouseUp(int x, int y)
+ {
+ upPressed = false;
+ downPressed = false;
+ IsPressed = false;
+ }
+
+ public void CommitEdit()
+ {
+ if (!IsEditing) return;
+ if (float.TryParse(currentInputString, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float parsed))
+ {
+ Value = System.Math.Max(Minimum, System.Math.Min(Maximum, parsed));
+ ValueChanged?.Invoke(this, EventArgs.Empty);
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ else if (float.TryParse(currentInputString.Replace(',', '.'), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float parsedAlt))
+ {
+ Value = System.Math.Max(Minimum, System.Math.Min(Maximum, parsedAlt));
+ ValueChanged?.Invoke(this, EventArgs.Empty);
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ IsEditing = false;
+ if (FocusedControl == this)
+ {
+ FocusedControl = null;
+ }
+ }
+
+ public override void KeyDown(OpenTK.Input.Key key)
+ {
+ if (!IsEditing) return;
+
+ if (key == OpenTK.Input.Key.Escape)
+ {
+ IsEditing = false;
+ if (FocusedControl == this) FocusedControl = null;
+ return;
+ }
+
+ if (key == OpenTK.Input.Key.Enter || key == OpenTK.Input.Key.KeypadEnter)
+ {
+ CommitEdit();
+ return;
+ }
+
+ if (key == OpenTK.Input.Key.BackSpace)
+ {
+ if (currentInputString.Length > 0)
+ {
+ currentInputString = currentInputString.Substring(0, currentInputString.Length - 1);
+ }
+ return;
+ }
+
+ string charToAdd = "";
+ if (key >= OpenTK.Input.Key.Number0 && key <= OpenTK.Input.Key.Number9)
+ {
+ charToAdd = ((int)(key - OpenTK.Input.Key.Number0)).ToString();
+ }
+ else if (key >= OpenTK.Input.Key.Keypad0 && key <= OpenTK.Input.Key.Keypad9)
+ {
+ charToAdd = ((int)(key - OpenTK.Input.Key.Keypad0)).ToString();
+ }
+ else if (key == OpenTK.Input.Key.Period || key == OpenTK.Input.Key.KeypadPeriod || key == OpenTK.Input.Key.Comma)
+ {
+ charToAdd = ".";
+ }
+ else if (key == OpenTK.Input.Key.Minus || key == OpenTK.Input.Key.KeypadMinus)
+ {
+ charToAdd = "-";
+ }
+
+ if (!string.IsNullOrEmpty(charToAdd))
+ {
+ currentInputString += charToAdd;
+ }
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLPanel.cs b/source/LibRender2/Primitives/GLPanel.cs
new file mode 100644
index 000000000..88b79d0df
--- /dev/null
+++ b/source/LibRender2/Primitives/GLPanel.cs
@@ -0,0 +1,21 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+
+namespace LibRender2.Primitives
+{
+ public class GLPanel : GLContainer
+ {
+ public GLPanel(BaseRenderer renderer) : base(renderer)
+ {
+ BackgroundColor = Color128.Black;
+ IsVisible = true;
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ Renderer.Rectangle.Draw(Texture, Location, Size, BackgroundColor);
+ base.Draw();
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLProgressBar.cs b/source/LibRender2/Primitives/GLProgressBar.cs
new file mode 100644
index 000000000..d07d4bab7
--- /dev/null
+++ b/source/LibRender2/Primitives/GLProgressBar.cs
@@ -0,0 +1,32 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+
+namespace LibRender2.Primitives
+{
+ public class GLProgressBar : GLControl
+ {
+ public float Value = 0f; // 0 to 1
+ public Color128 FillColor = Color128.Orange;
+ public Color128 TrackColor;
+
+ public GLProgressBar(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ BackgroundColor = new Color128(0.2f, 0.2f, 0.2f, 1f);
+ TrackColor = BackgroundColor;
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ // Draw track background
+ Renderer.Rectangle.Draw(null, Location, Size, TrackColor);
+ // Draw fill
+ double fillWidth = Size.X * System.Math.Max(0.0, System.Math.Min(1.0, Value));
+ if (fillWidth > 0)
+ {
+ Renderer.Rectangle.Draw(null, Location, new Vector2(fillWidth, Size.Y), FillColor);
+ }
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLScrollContainer.cs b/source/LibRender2/Primitives/GLScrollContainer.cs
new file mode 100644
index 000000000..748234c9c
--- /dev/null
+++ b/source/LibRender2/Primitives/GLScrollContainer.cs
@@ -0,0 +1,69 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+
+namespace LibRender2.Primitives
+{
+ public class GLScrollContainer : GLContainer
+ {
+ public double ScrollOffset = 0.0;
+ public double MaxScroll = 0.0;
+ public float ScrollbarWidth = 8f;
+ public Color128 ScrollbarColor = new Color128(0.4f, 0.4f, 0.45f, 1f);
+ public Color128 ScrollbarHandleColor = new Color128(0.6f, 0.6f, 0.65f, 1f);
+
+ public GLScrollContainer(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ ClipChildren = true;
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ double totalHeight = 0.0;
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (!Children[i].IsVisible) continue;
+ totalHeight += Children[i].Size.Y + 4.0;
+ }
+ MaxScroll = System.Math.Max(0.0, totalHeight - Size.Y);
+
+ // Draw children with scroll offset and clipping
+ for (int i = 0; i < Children.Count; i++)
+ {
+ if (!Children[i].IsVisible) continue;
+ Vector2 originalLocation = Children[i].Location;
+ Children[i].Location = new Vector2(originalLocation.X, originalLocation.Y - ScrollOffset);
+ // Only draw if visible within container bounds
+ if (Children[i].Location.Y + Children[i].Size.Y > Location.Y &&
+ Children[i].Location.Y < Location.Y + Size.Y)
+ {
+ Children[i].Draw();
+ }
+ Children[i].Location = originalLocation;
+ }
+
+ // Draw scrollbar if content overflows
+ if (MaxScroll > 0)
+ {
+ float scrollbarX = (float)(Location.X + Size.X - ScrollbarWidth);
+ // Track
+ Renderer.Rectangle.Draw(null, new Vector2(scrollbarX, (float)Location.Y), new Vector2(ScrollbarWidth, (float)Size.Y), ScrollbarColor);
+ // Handle
+ float handleHeight = (float)(Size.Y / (totalHeight) * Size.Y);
+ float handleY = (float)(Location.Y + (ScrollOffset / MaxScroll) * (Size.Y - handleHeight));
+ Renderer.Rectangle.Draw(null, new Vector2(scrollbarX, handleY), new Vector2(ScrollbarWidth, handleHeight), ScrollbarHandleColor);
+ }
+ }
+
+ public override void MouseWheel(int delta)
+ {
+ Scroll(delta);
+ }
+
+ public void Scroll(double delta)
+ {
+ ScrollOffset = System.Math.Max(0.0, System.Math.Min(MaxScroll, ScrollOffset + delta));
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLSlider.cs b/source/LibRender2/Primitives/GLSlider.cs
new file mode 100644
index 000000000..8fc541b26
--- /dev/null
+++ b/source/LibRender2/Primitives/GLSlider.cs
@@ -0,0 +1,69 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using System;
+
+namespace LibRender2.Primitives
+{
+ public class GLSlider : GLControl
+ {
+ public double Value = 0.0;
+ public double Minimum = 0.0;
+ public double Maximum = 100.0;
+ public EventHandler ValueChanged;
+
+ private bool dragging = false;
+
+ public GLSlider(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ Size = new Vector2(160, 20);
+ BackgroundColor = new Color128(0.2f, 0.2f, 0.2f, 1f);
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ double centerY = Location.Y + Size.Y / 2.0 - 2.0;
+ // Draw track background
+ Renderer.Rectangle.Draw(null, new Vector2(Location.X, centerY), new Vector2(Size.X, 4.0), BackgroundColor);
+ // Draw filled portion
+ double ratio = (Value - Minimum) / (Maximum - Minimum);
+ double fillWidth = ratio * Size.X;
+ Renderer.Rectangle.Draw(null, new Vector2(Location.X, centerY), new Vector2(fillWidth, 4.0), Color128.Orange);
+ // Draw knob
+ double knobX = Location.X + ratio * (Size.X - 12.0);
+ Color128 knobColor = dragging ? new Color128(1.0f, 0.7f, 0.2f, 1f) : Color128.Orange;
+ Renderer.Rectangle.Draw(null, new Vector2(knobX, Location.Y + 2.0), new Vector2(12.0, Size.Y - 4.0), knobColor);
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible) return;
+ CurrentlySelected = (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y);
+ if (dragging)
+ {
+ double localX = System.Math.Max(0.0, System.Math.Min(Size.X - 12.0, x - Location.X - 6.0));
+ double r = localX / (Size.X - 12.0);
+ Value = Minimum + r * (Maximum - Minimum);
+ ValueChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible) return;
+ if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ dragging = true;
+ IsPressed = true;
+ MouseMove(x, y);
+ }
+ }
+
+ public override void MouseUp(int x, int y)
+ {
+ dragging = false;
+ IsPressed = false;
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLTabContainer.cs b/source/LibRender2/Primitives/GLTabContainer.cs
new file mode 100644
index 000000000..2ee617533
--- /dev/null
+++ b/source/LibRender2/Primitives/GLTabContainer.cs
@@ -0,0 +1,148 @@
+using System.Collections.Generic;
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using OpenBveApi.Graphics;
+using System;
+
+namespace LibRender2.Primitives
+{
+ public class GLTabContainer : GLContainer
+ {
+ public List TabTitles = new List();
+ public List TabContents = new List();
+ public int SelectedTab = 0;
+
+ public float TabHeaderHeight = 30f;
+ public Color128 SelectedTabColor = new Color128(0.0f, 0.47f, 0.83f, 1.0f);
+ public Color128 InactiveTabColor = new Color128(0.15f, 0.15f, 0.15f, 1.0f);
+ public Color128 HoverTabColor = new Color128(0.2f, 0.2f, 0.24f, 1.0f);
+ public Color128 ContentBackgroundColor = new Color128(0.1f, 0.1f, 0.12f, 1.0f);
+
+ private int hoveredTab = -1;
+
+ public GLTabContainer(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ ClipChildren = true;
+ }
+
+ public void AddTab(string title, GLControl content)
+ {
+ TabTitles.Add(title);
+ TabContents.Add(content);
+ Children.Add(content);
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible || TabTitles.Count == 0) return;
+
+ // Draw tab headers
+ float tabWidth = (float)(Size.X / TabTitles.Count);
+ for (int i = 0; i < TabTitles.Count; i++)
+ {
+ Vector2 headerLoc = new Vector2(Location.X + i * tabWidth, Location.Y);
+ Vector2 headerSize = new Vector2(tabWidth, TabHeaderHeight);
+
+ bool isSelected = (i == SelectedTab);
+ bool isHovered = (i == hoveredTab);
+ Color128 headerBg;
+ if (isSelected)
+ {
+ headerBg = new Color128(0.2f, 0.22f, 0.25f, 0.9f);
+ }
+ else if (isHovered)
+ {
+ headerBg = HoverTabColor;
+ }
+ else
+ {
+ headerBg = InactiveTabColor;
+ }
+ Renderer.Rectangle.Draw(null, headerLoc, headerSize, headerBg);
+
+ // Draw active underline
+ if (isSelected)
+ {
+ Renderer.Rectangle.Draw(null, new Vector2(headerLoc.X, headerLoc.Y + TabHeaderHeight - 3f), new Vector2(tabWidth, 3f), SelectedTabColor);
+ }
+
+ // Draw text title centered
+ Vector2 textSize = Renderer.Fonts.NormalFont.MeasureString(TabTitles[i]);
+ Vector2 textPos = new Vector2(headerLoc.X + (tabWidth - textSize.X) / 2f, headerLoc.Y + (TabHeaderHeight - textSize.Y) / 2f);
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, TabTitles[i], textPos, TextAlignment.TopLeft, Color128.White);
+ }
+
+ // Draw content area background
+ Vector2 contentLoc = new Vector2(Location.X, Location.Y + TabHeaderHeight);
+ Vector2 contentSize = new Vector2(Size.X, Size.Y - TabHeaderHeight);
+ Renderer.Rectangle.Draw(null, contentLoc, contentSize, ContentBackgroundColor);
+
+ // Show only the selected tab's content
+ for (int i = 0; i < TabContents.Count; i++)
+ {
+ if (TabContents[i] == null) continue;
+
+ bool isSelected = (i == SelectedTab);
+ TabContents[i].IsVisible = isSelected;
+ if (isSelected)
+ {
+ // Align content to fit the tab area
+ TabContents[i].Location = contentLoc;
+ TabContents[i].Size = contentSize;
+ TabContents[i].Draw();
+ }
+ }
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible || TabTitles.Count == 0) return;
+
+ // Check if hovering over a tab header
+ if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + TabHeaderHeight)
+ {
+ float tabWidth = (float)(Size.X / TabTitles.Count);
+ hoveredTab = (int)((x - Location.X) / tabWidth);
+ if (hoveredTab < 0 || hoveredTab >= TabTitles.Count) hoveredTab = -1;
+ }
+ else
+ {
+ hoveredTab = -1;
+ }
+
+ // Pass to selected tab's content
+ base.MouseMove(x, y);
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible || TabTitles.Count == 0) return;
+
+ // Check if header clicked
+ if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + TabHeaderHeight)
+ {
+ float tabWidth = (float)(Size.X / TabTitles.Count);
+ int clickedTab = (int)((x - Location.X) / tabWidth);
+ if (clickedTab >= 0 && clickedTab < TabTitles.Count)
+ {
+ SelectedTab = clickedTab;
+ return;
+ }
+ }
+
+ // Otherwise pass to child contents
+ base.MouseDown(x, y);
+ }
+
+ public override void MouseUp(int x, int y)
+ {
+ base.MouseUp(x, y);
+ }
+
+ public override void MouseWheel(int delta)
+ {
+ base.MouseWheel(delta);
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLTextureRect.cs b/source/LibRender2/Primitives/GLTextureRect.cs
new file mode 100644
index 000000000..fb6070a5d
--- /dev/null
+++ b/source/LibRender2/Primitives/GLTextureRect.cs
@@ -0,0 +1,21 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using OpenBveApi.Textures;
+
+namespace LibRender2.Primitives
+{
+ public class GLTextureRect : GLControl
+ {
+ public GLTextureRect(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ BackgroundColor = Color128.White;
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible || Texture == null) return;
+ Renderer.Rectangle.Draw(Texture, Location, Size, BackgroundColor);
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/GLToggle.cs b/source/LibRender2/Primitives/GLToggle.cs
new file mode 100644
index 000000000..cee3c7456
--- /dev/null
+++ b/source/LibRender2/Primitives/GLToggle.cs
@@ -0,0 +1,76 @@
+using OpenBveApi.Math;
+using OpenBveApi.Colors;
+using System;
+using OpenBveApi.Graphics;
+
+namespace LibRender2.Primitives
+{
+ public class GLToggle : GLControl
+ {
+ public bool Checked;
+ public string Text = "";
+ public EventHandler ValueChanged;
+
+ /// Color for the toggle track background
+ public Color128 TrackColor = new Color128(0.3f, 0.3f, 0.3f, 1f);
+ /// Color for the toggle knob when checked
+ public Color128 KnobCheckedColor = Color128.Orange;
+ /// Color for the toggle knob when unchecked
+ public Color128 KnobUncheckedColor = new Color128(0.5f, 0.5f, 0.5f, 1f);
+
+ public GLToggle(BaseRenderer renderer) : base(renderer)
+ {
+ IsVisible = true;
+ Size = new Vector2(120, 24);
+ BackgroundColor = new Color128(0.1f, 0.1f, 0.1f, 1f);
+ }
+
+ public override void Draw()
+ {
+ if (!IsVisible) return;
+ // Draw toggle switch track
+ Color128 track = CurrentlySelected
+ ? new Color128(0.35f, 0.35f, 0.4f, 1f)
+ : TrackColor;
+ Renderer.Rectangle.Draw(null, Location, new Vector2(36, 18), track);
+
+ // Draw knob
+ float knobX = Checked ? (float)Location.X + 18f : (float)Location.X + 3f;
+ Color128 knobColor = Checked ? KnobCheckedColor : KnobUncheckedColor;
+ Renderer.Rectangle.Draw(null, new Vector2(knobX, (float)Location.Y + 2f), new Vector2(14, 14), knobColor);
+
+ // Draw label text
+ if (!string.IsNullOrEmpty(Text))
+ {
+ Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Text, new Vector2(Location.X + 44, Location.Y + 1), TextAlignment.TopLeft, Color128.White);
+ }
+ }
+
+ public override void MouseMove(int x, int y)
+ {
+ if (!IsVisible) return;
+ CurrentlySelected = (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y);
+ }
+
+ public override void MouseDown(int x, int y)
+ {
+ if (!IsVisible) return;
+ if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ IsPressed = true;
+ }
+ }
+
+ public override void MouseUp(int x, int y)
+ {
+ if (!IsVisible) return;
+ if (IsPressed && x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y)
+ {
+ Checked = !Checked;
+ ValueChanged?.Invoke(this, EventArgs.Empty);
+ OnClick?.Invoke(this, EventArgs.Empty);
+ }
+ IsPressed = false;
+ }
+ }
+}
diff --git a/source/LibRender2/Primitives/Label.cs b/source/LibRender2/Primitives/Label.cs
index 710fbc4a6..c1193d791 100644
--- a/source/LibRender2/Primitives/Label.cs
+++ b/source/LibRender2/Primitives/Label.cs
@@ -1,4 +1,4 @@
-//Simplified BSD License (BSD-2-Clause)
+//Simplified BSD License (BSD-2-Clause)
//
//Copyright (c) 2022, Christopher Lees, The OpenBVE Project
//
@@ -25,6 +25,7 @@
using LibRender2.Text;
using OpenBveApi.Colors;
using OpenBveApi.Graphics;
+using OpenBveApi.Math;
namespace LibRender2.Primitives
{
@@ -40,17 +41,19 @@ public class Label : GLControl
public Label(BaseRenderer renderer, string text) : base(renderer)
{
Text = text;
- Font = Renderer.Fonts.LargeFont;
- Size = Font.MeasureString(Text) * 1.5;
+ Font = Renderer.Fonts.NormalFont;
+ Size = Font.MeasureString(Text);
// default colors to match GLMenu
BackgroundColor = Color128.Black;
TextColor = Color128.White;
+ IsVisible = true;
}
public override void Draw()
{
Renderer.Rectangle.Draw(Texture, Location, Size, BackgroundColor);
- Renderer.OpenGlString.Draw(Font, Text, Location + (Size * 0.15), TextAlignment.TopLeft, TextColor);
+ float textY = (float)(Location.Y + (Size.Y - Font.MeasureString(Text).Y) / 2.0f);
+ Renderer.OpenGlString.Draw(Font, Text, new Vector2(Location.X, textY), TextAlignment.TopLeft, TextColor);
}
}
}
diff --git a/source/LibRender2/Text/OpenGlString.cs b/source/LibRender2/Text/OpenGlString.cs
index 239c54db8..70aae04e1 100644
--- a/source/LibRender2/Text/OpenGlString.cs
+++ b/source/LibRender2/Text/OpenGlString.cs
@@ -144,8 +144,8 @@ private void DrawImmediate(string text, OpenGlFont font, double left, double top
{
GL.BindTexture(TextureTarget.Texture2D, texture.OpenGlTextures[(int)OpenGlTextureWrapMode.ClampClamp].Name);
- double x = left - (data.PhysicalSize.X - data.TypographicSize.X) / 2;
- double y = top - (data.PhysicalSize.Y - data.TypographicSize.Y) / 2;
+ double x = System.Math.Round(left - (data.PhysicalSize.X - data.TypographicSize.X) / 2);
+ double y = System.Math.Round(top - (data.PhysicalSize.Y - data.TypographicSize.Y) / 2);
/*
* In the first pass, mask off the background with pure black.
@@ -206,8 +206,8 @@ private void DrawWithShader(string text, OpenGlFont font, double left, double to
{
GL.BindTexture(TextureTarget.Texture2D, texture.OpenGlTextures[(int)OpenGlTextureWrapMode.ClampClamp].Name);
Shader.SetAtlasLocation(data.TextureCoordinates);
- double x = left - (data.PhysicalSize.X - data.TypographicSize.X) / 2;
- double y = top - (data.PhysicalSize.Y - data.TypographicSize.Y) / 2;
+ double x = System.Math.Round(left - (data.PhysicalSize.X - data.TypographicSize.X) / 2);
+ double y = System.Math.Round(top - (data.PhysicalSize.Y - data.TypographicSize.Y) / 2);
/*
* In the first pass, mask off the background with pure black.
diff --git a/source/ObjectViewer/Game/Menu.SingleMenu.cs b/source/ObjectViewer/Game/Menu.SingleMenu.cs
index 8e2834e12..7f5a197c9 100644
--- a/source/ObjectViewer/Game/Menu.SingleMenu.cs
+++ b/source/ObjectViewer/Game/Menu.SingleMenu.cs
@@ -148,25 +148,7 @@ public SingleMenu(AbstractMenu menu, MenuType menuType, int data = 0, double max
Align = TextAlignment.TopLeft;
break;
case MenuType.Options:
- Items = new MenuEntry[9];
- Items[0] = new MenuCaption(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "panel", "options" }));
- Items[1] = new MenuOption(menu, OptionType.ScreenResolution, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "resolution" }), Program.Renderer.Screen.AvailableResolutions.ToArray());
- Items[2] = new MenuOption(menu, OptionType.FullScreen, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "display_mode_fullscreen" }), new[] { "true", "false" });
- Items[3] = new MenuOption(menu, OptionType.Interpolation, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation" }), new[]
- {
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearest"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinear"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearestmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinearmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_trilinearmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_anisotropic"})
- });
- Items[4] = new MenuOption(menu, OptionType.AnisotropicLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation_anisotropic_level" }), new[] { "0", "2", "4", "8", "16" });
- Items[5] = new MenuOption(menu, OptionType.AntialiasingLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation_antialiasing_level" }), new[] { "0", "2", "4", "8", "16" });
- Items[6] = new MenuOption(menu, OptionType.ViewingDistance, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_distance_viewingdistance" }), new[] { "400", "600", "800", "1000", "1500", "2000" });
- Items[7] = new MenuOption(menu, OptionType.AutoReloadObjects, "Automatically Reload Objects", new[] { "true", "false"});
- Items[8] = new MenuCommand(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "menu", "back" }), MenuTag.MenuBack, 0);
-
+ Items = menu.CreateOptionsEntries(HostApplication.ObjectViewer);
Align = TextAlignment.TopLeft;
break;
case MenuType.ErrorList:
diff --git a/source/ObjectViewer/Game/Menu.cs b/source/ObjectViewer/Game/Menu.cs
index 126994505..5a0cf5b0b 100644
--- a/source/ObjectViewer/Game/Menu.cs
+++ b/source/ObjectViewer/Game/Menu.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using LibRender2.Menu;
using LibRender2.Primitives;
@@ -19,7 +19,6 @@ public sealed partial class GameMenu: AbstractMenu
internal Picturebox filePictureBox;
internal Textbox fileTextBox;
- private double lastTimeElapsed;
private static string SearchDirectory;
private static string currentFile;
@@ -62,9 +61,25 @@ public override void Initialize()
filePictureBox.Size = new Vector2(quarterWidth, quarterWidth);
filePictureBox.BackgroundColor = Color128.White;
SearchDirectory = Interface.CurrentOptions.ObjectSearchDirectory;
+
+ menuControls.Add(filePictureBox);
+ menuControls.Add(fileTextBox);
+ InitializeOptionsTabContainer(HostApplication.ObjectViewer);
IsInitialized = true;
}
+ public override void RepositionSidebarControls()
+ {
+ if (!IsInitialized) return;
+ base.RepositionSidebarControls();
+ double startX = menuMin.X;
+ double size = SidebarWidth - 32;
+ filePictureBox.Location = new Vector2(startX + 16, Renderer.Screen.Height - size - 150);
+ filePictureBox.Size = new Vector2(size, size);
+ fileTextBox.Location = new Vector2(startX + 16, Renderer.Screen.Height - 140);
+ fileTextBox.Size = new Vector2(size, 120);
+ }
+
public override void PushMenu(MenuType type, int data = 0, bool replace = false)
{
if (Renderer.CurrentInterface < InterfaceType.Menu)
@@ -92,6 +107,13 @@ public override void PushMenu(MenuType type, int data = 0, bool replace = false)
Menus[CurrMenu].Selection = 1;
}
ComputePosition();
+ if (IsSidebarMode)
+ {
+ SidebarVisible = true;
+ startOffset = currentOffset;
+ animationElapsed = 0.0;
+ isAnimating = true;
+ }
Renderer.CurrentInterface = InterfaceType.Menu;
}
@@ -220,11 +242,33 @@ public override void ProcessCommand(Translations.Command cmd, double timeElapsed
public override bool ProcessMouseMove(int x, int y)
{
+ if (HandleSidebarResizeMove(x, y))
+ {
+ return true;
+ }
Program.Renderer.GameWindow.CursorVisible = true;
if (CurrMenu < 0)
{
return false;
}
+ if (Menus[CurrMenu].Type == MenuType.Options)
+ {
+ if (OptionsTabContainer != null && OptionsTabContainer.IsVisible)
+ {
+ OptionsTabContainer.MouseMove(x, y);
+ return true;
+ }
+ }
+ if (isScrubbing && scrubbingOption != null)
+ {
+ int deltaX = x - lastMouseX;
+ if (deltaX != 0)
+ {
+ scrubbingOption.ApplyScrubDelta(deltaX);
+ lastMouseX = x;
+ }
+ return true;
+ }
// if not in menu or during control customisation or down outside menu area, do nothing
if (Renderer.CurrentInterface < InterfaceType.Menu)
return false;
@@ -268,24 +312,40 @@ public override bool ProcessMouseMove(int x, int y)
public override void Draw(double RealTimeElapsed)
{
- double TimeElapsed = RealTimeElapsed - lastTimeElapsed;
- lastTimeElapsed = RealTimeElapsed;
+ double TimeElapsed = RealTimeElapsed;
int i;
+ UpdateTransition(TimeElapsed);
+
if (CurrMenu < 0 || CurrMenu >= Menus.Length)
+ {
+ DrawSidebarToggleButton(RealTimeElapsed);
return;
+ }
MenuBase menu = Menus[CurrMenu];
// overlay background
Renderer.Rectangle.Draw(null, Vector2.Null, new Vector2(Renderer.Screen.Width, Renderer.Screen.Height), overlayColor);
+ if (menu.Type == MenuType.Options)
+ {
+ Renderer.Rectangle.Draw(null, new Vector2(menuMin.X, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
+ if (OptionsTabContainer != null)
+ {
+ OptionsTabContainer.Draw();
+ LibRender2.Primitives.GLDropdown.ActiveDropdown?.DrawOverlay();
+ }
+ DrawSidebarToggleButton(RealTimeElapsed);
+ return;
+ }
+
double itemLeft, itemX;
if (menu.Align == TextAlignment.TopLeft)
{
- itemLeft = 0;
- itemX = 16;
- Renderer.Rectangle.Draw(null, new Vector2(0, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
+ itemLeft = menuMin.X;
+ itemX = menuMin.X + 16;
+ Renderer.Rectangle.Draw(null, new Vector2(menuMin.X, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
}
else
{
@@ -369,8 +429,10 @@ public override void Draw(double RealTimeElapsed)
menu.Align, ColourNormal, false);
if (menu.Items[i] is MenuOption opt)
{
- Renderer.OpenGlString.Draw(MenuFont, opt.CurrentOption.ToString(), new Vector2((menuMax.X - menuMin.X + 2.0f * Border.X) + 4.0f, itemY),
- menu.Align, backgroundColor, false);
+ Color128 optColor = (i == menu.Selection) ? ColourHighlight : ColourNormal;
+ double optX = IsSidebarMode ? (menuMax.X - 120.0f) : ((menuMax.X - menuMin.X + 2.0f * Border.X) + 4.0f);
+ Renderer.OpenGlString.Draw(MenuFont, opt.DisplayValue, new Vector2(optX, itemY),
+ IsSidebarMode ? TextAlignment.TopLeft : menu.Align, optColor, false);
}
itemY += LineHeight;
if (menu.Items[i].Icon != null)
@@ -390,6 +452,9 @@ public override void Draw(double RealTimeElapsed)
if (i < menu.Items.Length - 1)
Renderer.OpenGlString.Draw(MenuFont, @"...", new Vector2(itemX, itemY),
menu.Align, ColourDimmed, false);
+
+ DrawSidebarToggleButton(RealTimeElapsed);
+ DrawSidebarResizeGrip();
}
}
}
diff --git a/source/ObjectViewer/Graphics/NewRendererS.cs b/source/ObjectViewer/Graphics/NewRendererS.cs
index e70997c0a..35bd2cf01 100644
--- a/source/ObjectViewer/Graphics/NewRendererS.cs
+++ b/source/ObjectViewer/Graphics/NewRendererS.cs
@@ -387,7 +387,7 @@ private void RenderOverlays(double timeElapsed)
}
}
}
- if (CurrentInterface == InterfaceType.Menu)
+ if (CurrentInterface == InterfaceType.Menu || Game.Menu.IsSidebarMode)
{
Game.Menu.Draw(timeElapsed);
}
diff --git a/source/ObjectViewer/ProgramS.cs b/source/ObjectViewer/ProgramS.cs
index 802b8cfe5..43bd3e604 100644
--- a/source/ObjectViewer/ProgramS.cs
+++ b/source/ObjectViewer/ProgramS.cs
@@ -222,6 +222,15 @@ internal static void MouseMoveEvent(object sender, MouseMoveEventArgs e)
internal static void MouseEvent(object sender, MouseButtonEventArgs e)
{
+ if (e.IsPressed && Game.Menu != null && Game.Menu.IsSidebarMode)
+ {
+ OpenBveApi.Math.Vector4 rect = Game.Menu.GetToggleButtonRect();
+ if (e.X >= rect.X && e.X <= rect.X + rect.Z && e.Y >= rect.Y && e.Y <= rect.Y + rect.W)
+ {
+ Game.Menu.ProcessMouseDown(e.X, e.Y);
+ return;
+ }
+ }
switch (Program.Renderer.CurrentInterface)
{
case InterfaceType.Menu:
@@ -231,6 +240,10 @@ internal static void MouseEvent(object sender, MouseButtonEventArgs e)
// viewer hooks up and down to same event
Game.Menu.ProcessMouseDown(e.X, e.Y);
}
+ else
+ {
+ Game.Menu.ProcessMouseUp(e.X, e.Y);
+ }
break;
default:
MouseCameraPosition = Renderer.Camera.AbsolutePosition;
@@ -538,7 +551,14 @@ private static void OnFileChanged(object source, System.IO.FileSystemEventArgs e
// process events
internal static void KeyDown(object sender, KeyboardKeyEventArgs e)
{
-
+ if (Renderer.CurrentInterface != InterfaceType.Normal)
+ {
+ Game.Menu.ProcessKeyDown(e.Key);
+ if (e.Key != Key.Up && e.Key != Key.Down && e.Key != Key.Enter && e.Key != Key.Escape && e.Key != Key.Left && e.Key != Key.Right)
+ {
+ return;
+ }
+ }
switch (e.Key)
{
case Key.LShift:
diff --git a/source/ObjectViewer/System/GameWindow.cs b/source/ObjectViewer/System/GameWindow.cs
index 67791562a..52bcdc671 100644
--- a/source/ObjectViewer/System/GameWindow.cs
+++ b/source/ObjectViewer/System/GameWindow.cs
@@ -297,6 +297,7 @@ protected override void OnResize(EventArgs e)
Program.Renderer.Screen.Width = Width;
Program.Renderer.Screen.Height = Height;
Program.Renderer.UpdateViewport(ViewportChangeMode.NoChange);
+ Game.Menu.ComputePosition();
}
protected override void OnLoad(EventArgs e)
diff --git a/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs b/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs
index c627725e4..a174872f7 100644
--- a/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs
+++ b/source/OpenBVE/Game/Menu/Menu.SingleMenu.cs
@@ -224,34 +224,7 @@ public SingleMenu(AbstractMenu menu, MenuType menuType, int data = 0, double Max
Align = TextAlignment.TopLeft;
break;
case MenuType.Options:
- Items = new MenuEntry[12];
- Items[0] = new MenuCaption(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"panel","options"}));
- Items[1] = new MenuOption(menu, OptionType.ScreenResolution, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","resolution"}), Program.Renderer.Screen.AvailableResolutions.ToArray());
- Items[2] = new MenuOption(menu, OptionType.FullScreen, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","display_mode_fullscreen"}), new[] { "true", "false" });
- Items[3] = new MenuOption(menu, OptionType.Interpolation, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation"}), new[]
- {
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearest"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinear"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearestmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinearmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_trilinearmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_anisotropic"})
- });
- Items[4] = new MenuOption(menu, OptionType.AnisotropicLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_anisotropic_level"}), new[] { "0", "2", "4", "8", "16" });
- Items[5] = new MenuOption(menu, OptionType.AntialiasingLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_antialiasing_level"}), new[] { "0", "2", "4", "8", "16" });
- Items[6] = new MenuOption(menu, OptionType.ViewingDistance, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_distance_viewingdistance"}), new[] { "400", "600", "800", "1000", "1500", "2000" });
- Items[7] = new MenuOption(menu, OptionType.UIScaleFactor, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "ui_scalefactor" }), new[] { "1x", "2x", "3x", "4x", "5x", "6x" });
- Items[8] = new MenuOption(menu, OptionType.NumberOfSounds, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "misc_sound_number" }), new[] { "16", "32", "64", "128"});
- Items[9] = new MenuOption(menu, OptionType.ShadowQuality, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution" }), new[]
- {
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution_off" }),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution_low" }),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution_medium" }),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution_high" }),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "shadows_resolution_ultra" })
- });
- Items[10] = new MenuOption(menu, OptionType.ShadowFilterCascades, "Per-cascade culling", new[] { "true", "false" });
- Items[11] = new MenuCommand(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"menu","back"}), MenuTag.MenuBack, 0);
+ Items = menu.CreateOptionsEntries(HostApplication.OpenBve);
Align = TextAlignment.TopLeft;
break;
case MenuType.RouteList:
diff --git a/source/OpenBVE/Game/Menu/Menu.cs b/source/OpenBVE/Game/Menu/Menu.cs
index 86fe5527f..1091b5745 100644
--- a/source/OpenBVE/Game/Menu/Menu.cs
+++ b/source/OpenBVE/Game/Menu/Menu.cs
@@ -64,6 +64,18 @@ private GameMenu() : base(Program.Renderer, Interface.CurrentOptions)
{
}
+ public override void OnOptionChanged(OptionType type)
+ {
+ if (type == OptionType.ViewingDistance)
+ {
+ if (Program.CurrentRoute != null && Program.CurrentRoute.CurrentBackground != null)
+ {
+ Program.CurrentRoute.CurrentBackground.BackgroundImageDistance = Interface.CurrentOptions.ViewingDistance;
+ }
+ Program.Renderer.UpdateViewingDistances(Interface.CurrentOptions.ViewingDistance);
+ }
+ }
+
/// Returns the current menu instance (If applicable)
public static readonly GameMenu Instance = new GameMenu();
@@ -98,6 +110,21 @@ public override void Initialize()
menuControls.Add(nextImageButton);
menuControls.Add(previousImageButton);
menuControls.Add(nextStepButton);
+ InitializeOptionsTabContainer(HostApplication.OpenBve);
+ }
+
+ public override void RepositionSidebarControls()
+ {
+ if (!IsInitialized) return;
+ base.RepositionSidebarControls();
+ double startX = menuMin.X;
+ double size = SidebarWidth - 32;
+ routePictureBox.Location = new Vector2(startX + 16, Renderer.Screen.Height - size - 150);
+ routePictureBox.Size = new Vector2(size, size);
+ routeDescriptionBox.Location = new Vector2(startX + 16, Renderer.Screen.Height - 140);
+ routeDescriptionBox.Size = new Vector2(size, 120);
+ LogoPictureBox.Location = new Vector2(startX + 16, 20);
+ LogoPictureBox.Size = new Vector2(size, size / 2);
}
/// Should be called when the screen resolution changes to re-position all items on the menu appropriately
@@ -207,6 +234,13 @@ public override void PushMenu(MenuType type, int data = 0, bool replace = false
Menus[CurrMenu].Selection = 1;
}
ComputePosition();
+ if (IsSidebarMode)
+ {
+ SidebarVisible = true;
+ startOffset = currentOffset;
+ animationElapsed = 0.0;
+ isAnimating = true;
+ }
Program.Renderer.CurrentInterface = TrainManagerBase.PlayerTrain == null ? InterfaceType.GLMainMenu : InterfaceType.Menu;
}
@@ -298,6 +332,24 @@ public override bool ProcessMouseMove(int x, int y)
{
return false;
}
+ if (Menus[CurrMenu].Type == MenuType.Options)
+ {
+ if (OptionsTabContainer != null && OptionsTabContainer.IsVisible)
+ {
+ OptionsTabContainer.MouseMove(x, y);
+ return true;
+ }
+ }
+ if (isScrubbing && scrubbingOption != null)
+ {
+ int deltaX = x - lastMouseX;
+ if (deltaX != 0)
+ {
+ scrubbingOption.ApplyScrubDelta(deltaX);
+ lastMouseX = x;
+ }
+ return true;
+ }
// if not in menu or during control customisation or down outside menu area, do nothing
if (Program.Renderer.CurrentInterface < InterfaceType.Menu ||
isCustomisingControl)
@@ -750,24 +802,44 @@ public override void Draw(double RealTimeElapsed)
TrainManagerBase.PlayerTrain.Plugin.KeepAlive();
pluginKeepAliveTimer = 0;
}
- double TimeElapsed = RealTimeElapsed - lastTimeElapsed;
- lastTimeElapsed = RealTimeElapsed;
+ double TimeElapsed = RealTimeElapsed;
int i;
+ UpdateTransition(TimeElapsed);
+
if (CurrMenu < 0 || CurrMenu >= Menus.Length)
+ {
+ DrawSidebarToggleButton(RealTimeElapsed);
+ Renderer.PopMatrix(MatrixMode.Modelview);
+ Renderer.PopMatrix(MatrixMode.Projection);
return;
+ }
MenuBase menu = Menus[CurrMenu];
// overlay background
Program.Renderer.Rectangle.Draw(null, Vector2.Null, new Vector2(Program.Renderer.Screen.Width, Program.Renderer.Screen.Height), overlayColor);
+ if (menu.Type == MenuType.Options)
+ {
+ Program.Renderer.Rectangle.Draw(null, new Vector2(menuMin.X, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
+ if (OptionsTabContainer != null)
+ {
+ OptionsTabContainer.Draw();
+ LibRender2.Primitives.GLDropdown.ActiveDropdown?.DrawOverlay();
+ }
+ DrawSidebarToggleButton(RealTimeElapsed);
+ Renderer.PopMatrix(MatrixMode.Modelview);
+ Renderer.PopMatrix(MatrixMode.Projection);
+ return;
+ }
+
double itemLeft, itemX;
if (menu.Align == TextAlignment.TopLeft)
{
- itemLeft = 0;
- itemX = 16;
- Program.Renderer.Rectangle.Draw(null, new Vector2(0, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
+ itemLeft = menuMin.X;
+ itemX = menuMin.X + 16;
+ Program.Renderer.Rectangle.Draw(null, new Vector2(menuMin.X, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
}
else
{
@@ -851,8 +923,10 @@ public override void Draw(double RealTimeElapsed)
menu.Align, ColourNormal, false);
if (menu.Items[i] is MenuOption opt)
{
- Program.Renderer.OpenGlString.Draw(MenuFont, opt.CurrentOption.ToString(), new Vector2((menuMax.X - menuMin.X + 2.0f * Border.X) + 4.0f, itemY),
- menu.Align, backgroundColor, false);
+ Color128 optColor = (i == menu.Selection) ? ColourHighlight : ColourNormal;
+ double optX = IsSidebarMode ? (menuMax.X - 120.0f) : ((menuMax.X - menuMin.X + 2.0f * Border.X) + 4.0f);
+ Program.Renderer.OpenGlString.Draw(MenuFont, opt.DisplayValue, new Vector2(optX, itemY),
+ IsSidebarMode ? TextAlignment.TopLeft : menu.Align, optColor, false);
}
itemY += LineHeight;
if (menu.Items[i].Icon != null)
@@ -999,6 +1073,7 @@ public override void Draw(double RealTimeElapsed)
switchMapPictureBox.Draw();
break;
}
+ DrawSidebarToggleButton(RealTimeElapsed);
Renderer.PopMatrix(MatrixMode.Modelview);
Renderer.PopMatrix(MatrixMode.Projection);
}
diff --git a/source/OpenBVE/Graphics/NewRenderer.cs b/source/OpenBVE/Graphics/NewRenderer.cs
index fb98d8996..c3fec26a3 100644
--- a/source/OpenBVE/Graphics/NewRenderer.cs
+++ b/source/OpenBVE/Graphics/NewRenderer.cs
@@ -468,15 +468,13 @@ internal void RenderScene(double TimeElapsed, double RealTimeElapsed)
SetBlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); //FIXME: Remove when text switches between two renderer types
GL.Disable(EnableCap.DepthTest);
overlays.Render(RealTimeElapsed);
- switch (CurrentInterface)
+ if (CurrentInterface == InterfaceType.Menu || CurrentInterface == InterfaceType.GLMainMenu || Game.Menu.IsSidebarMode)
{
- case InterfaceType.Menu:
- case InterfaceType.GLMainMenu:
- Game.Menu.Draw(TimeElapsed);
- break;
- case InterfaceType.SwitchChangeMap:
- Game.SwitchChangeDialog.Draw();
- break;
+ Game.Menu.Draw(TimeElapsed);
+ }
+ else if (CurrentInterface == InterfaceType.SwitchChangeMap)
+ {
+ Game.SwitchChangeDialog.Draw();
}
OptionLighting = true;
}
diff --git a/source/OpenBVE/System/Input/Keyboard.cs b/source/OpenBVE/System/Input/Keyboard.cs
index 2a0de2933..e95b56595 100644
--- a/source/OpenBVE/System/Input/Keyboard.cs
+++ b/source/OpenBVE/System/Input/Keyboard.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using LibRender2.Menu;
using LibRender2.Screens;
using OpenTK.Input;
@@ -11,6 +11,21 @@ internal static partial class MainLoop
/// Called when a KeyDown event is generated
internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e)
{
+ if (Program.Renderer.CurrentInterface != InterfaceType.Normal)
+ {
+ Game.Menu.ProcessKeyDown((OpenTK.Input.Key)e.Key);
+ if (e.Key != Key.Up && e.Key != Key.Down && e.Key != Key.Enter && e.Key != Key.Escape && e.Key != Key.Left && e.Key != Key.Right)
+ {
+ if (Game.Menu.IsCustomizingControl())
+ {
+ // let it fall through
+ }
+ else
+ {
+ return;
+ }
+ }
+ }
if (Interface.CurrentOptions.KioskMode && Program.Renderer.CurrentInterface != InterfaceType.GLMainMenu)
{
//If in kiosk mode, reset the timer and disable AI on keypress
diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs
index 3abbdf1ca..ab64447f7 100644
--- a/source/OpenBVE/System/MainLoop.cs
+++ b/source/OpenBVE/System/MainLoop.cs
@@ -147,6 +147,15 @@ internal static void mouseDownEvent(object sender, MouseButtonEventArgs e)
}
if (e.Button == MouseButton.Left)
{
+ if (Game.Menu != null && Game.Menu.IsSidebarMode)
+ {
+ OpenBveApi.Math.Vector4 rect = Game.Menu.GetToggleButtonRect();
+ if (e.X >= rect.X && e.X <= rect.X + rect.Z && e.Y >= rect.Y && e.Y <= rect.Y + rect.W)
+ {
+ Game.Menu.ProcessMouseDown(e.X, e.Y);
+ return;
+ }
+ }
switch (Program.Renderer.CurrentInterface)
{
case InterfaceType.Normal:
@@ -178,6 +187,10 @@ internal static void mouseUpEvent(object sender, MouseButtonEventArgs e)
{
Program.Renderer.Touch.LeaveCheck(new Vector2(e.X, e.Y));
}
+ else if (Program.Renderer.CurrentInterface == InterfaceType.Menu || Program.Renderer.CurrentInterface == InterfaceType.GLMainMenu)
+ {
+ Game.Menu.ProcessMouseUp(e.X, e.Y);
+ }
}
}
diff --git a/source/OpenBveApi/System/BaseOptions.cs b/source/OpenBveApi/System/BaseOptions.cs
index 135f66022..03bfa074e 100644
--- a/source/OpenBveApi/System/BaseOptions.cs
+++ b/source/OpenBveApi/System/BaseOptions.cs
@@ -96,6 +96,12 @@ public abstract class BaseOptions
public double ShadowNormalBias = 2.0;
/// Whether to filter shadow casters per cascade to improve performance.
public bool ShadowFilterCascades = true;
+ /// Whether to show the loading progress bar
+ public bool LoadingProgressBar = true;
+ /// Whether to show the loading logo
+ public bool LoadingLogo = true;
+ /// Whether to show the loading background
+ public bool LoadingBackground = true;
/// The sun azimuth in degrees
diff --git a/source/RouteViewer/Game/Menu.SingleMenu.cs b/source/RouteViewer/Game/Menu.SingleMenu.cs
index 75c2b2067..a90f122b6 100644
--- a/source/RouteViewer/Game/Menu.SingleMenu.cs
+++ b/source/RouteViewer/Game/Menu.SingleMenu.cs
@@ -1,4 +1,4 @@
-using LibRender2.Menu;
+using LibRender2.Menu;
using OpenBveApi.Graphics;
using OpenBveApi.Hosts;
using OpenBveApi.Interface;
@@ -142,23 +142,7 @@ public SingleMenu(AbstractMenu menu, MenuType menuType, int data = 0, double max
Align = TextAlignment.TopLeft;
break;
case MenuType.Options:
- Items = new MenuEntry[8];
- Items[0] = new MenuCaption(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "panel", "options" }));
- Items[1] = new MenuOption(menu, OptionType.ScreenResolution, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "resolution" }), Program.Renderer.Screen.AvailableResolutions.ToArray());
- Items[2] = new MenuOption(menu, OptionType.FullScreen, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "display_mode_fullscreen" }), new[] { "true", "false" });
- Items[3] = new MenuOption(menu, OptionType.Interpolation, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation" }), new[]
- {
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearest"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinear"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_nearestmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_bilinearmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_trilinearmipmap"}),
- Translations.GetInterfaceString(HostApplication.OpenBve, new[] {"options","quality_interpolation_mode_anisotropic"})
- });
- Items[4] = new MenuOption(menu, OptionType.AnisotropicLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation_anisotropic_level" }), new[] { "0", "2", "4", "8", "16" });
- Items[5] = new MenuOption(menu, OptionType.AntialiasingLevel, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_interpolation_antialiasing_level" }), new[] { "0", "2", "4", "8", "16" });
- Items[6] = new MenuOption(menu, OptionType.ViewingDistance, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "options", "quality_distance_viewingdistance" }), new[] { "400", "600", "800", "1000", "1500", "2000" });
- Items[7] = new MenuCommand(menu, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "menu", "back" }), MenuTag.MenuBack, 0);
+ Items = menu.CreateOptionsEntries(HostApplication.RouteViewer);
Align = TextAlignment.TopLeft;
break;
case MenuType.ErrorList:
diff --git a/source/RouteViewer/Game/Menu.cs b/source/RouteViewer/Game/Menu.cs
index bc88134b6..9d5b6607d 100644
--- a/source/RouteViewer/Game/Menu.cs
+++ b/source/RouteViewer/Game/Menu.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
using System.IO;
using System.Text;
@@ -22,7 +22,6 @@ namespace RouteViewer
{
public sealed partial class GameMenu : AbstractMenu
{
- private double lastTimeElapsed;
private static string SearchDirectory;
private static string currentFile;
private static RouteState RoutefileState;
@@ -38,6 +37,18 @@ internal GameMenu() : base(Program.Renderer, Interface.CurrentOptions)
{
}
+ public override void OnOptionChanged(OptionType type)
+ {
+ if (type == OptionType.ViewingDistance)
+ {
+ if (Program.CurrentRoute != null && Program.CurrentRoute.CurrentBackground != null)
+ {
+ Program.CurrentRoute.CurrentBackground.BackgroundImageDistance = Interface.CurrentOptions.ViewingDistance;
+ }
+ Program.Renderer.UpdateViewingDistances(Interface.CurrentOptions.ViewingDistance);
+ }
+ }
+
public override void Initialize()
{
routePictureBox = new Picturebox(Renderer);
@@ -71,9 +82,24 @@ public override void Initialize()
routePictureBox.BackgroundColor = Color128.White;
SearchDirectory = Interface.CurrentOptions.RouteSearchDirectory;
+ menuControls.Add(routePictureBox);
+ menuControls.Add(routeDescriptionBox);
+ InitializeOptionsTabContainer(HostApplication.RouteViewer);
IsInitialized = true;
}
+ public override void RepositionSidebarControls()
+ {
+ if (!IsInitialized) return;
+ base.RepositionSidebarControls();
+ double startX = menuMin.X;
+ double size = SidebarWidth - 32;
+ routePictureBox.Location = new Vector2(startX + 16, Renderer.Screen.Height - size - 150);
+ routePictureBox.Size = new Vector2(size, size);
+ routeDescriptionBox.Location = new Vector2(startX + 16, Renderer.Screen.Height - 140);
+ routeDescriptionBox.Size = new Vector2(size, 120);
+ }
+
public override void PushMenu(MenuType type, int data = 0, bool replace = false)
{
if (Renderer.CurrentInterface < InterfaceType.Menu)
@@ -101,6 +127,13 @@ public override void PushMenu(MenuType type, int data = 0, bool replace = false)
Menus[CurrMenu].Selection = 1;
}
ComputePosition();
+ if (IsSidebarMode)
+ {
+ SidebarVisible = true;
+ startOffset = currentOffset;
+ animationElapsed = 0.0;
+ isAnimating = true;
+ }
Renderer.CurrentInterface = InterfaceType.Menu;
}
@@ -244,11 +277,33 @@ public override void ProcessCommand(Translations.Command cmd, double timeElapsed
public override bool ProcessMouseMove(int x, int y)
{
+ if (HandleSidebarResizeMove(x, y))
+ {
+ return true;
+ }
Program.Renderer.GameWindow.CursorVisible = true;
if (CurrMenu < 0)
{
return false;
}
+ if (Menus[CurrMenu].Type == MenuType.Options)
+ {
+ if (OptionsTabContainer != null && OptionsTabContainer.IsVisible)
+ {
+ OptionsTabContainer.MouseMove(x, y);
+ return true;
+ }
+ }
+ if (isScrubbing && scrubbingOption != null)
+ {
+ int deltaX = x - lastMouseX;
+ if (deltaX != 0)
+ {
+ scrubbingOption.ApplyScrubDelta(deltaX);
+ lastMouseX = x;
+ }
+ return true;
+ }
// if not in menu or during control customisation or down outside menu area, do nothing
if (Renderer.CurrentInterface < InterfaceType.Menu)
return false;
@@ -265,7 +320,7 @@ public override bool ProcessMouseMove(int x, int y)
{
routeDescriptionBox.MouseMove(x, y);
//HACK: Use this to trigger our menu start button!
- if (x > Renderer.Screen.Width - 200 && x < Renderer.Screen.Width - 10 && y > Renderer.Screen.Height - 40 && y < Renderer.Screen.Height - 10)
+ if (x > menuMin.X + SidebarWidth - 200 && x < menuMin.X + SidebarWidth - 10 && y > Renderer.Screen.Height - 40 && y < Renderer.Screen.Height - 10)
{
menu.Selection = int.MaxValue;
return true;
@@ -293,24 +348,40 @@ public override bool ProcessMouseMove(int x, int y)
public override void Draw(double RealTimeElapsed)
{
- double TimeElapsed = RealTimeElapsed - lastTimeElapsed;
- lastTimeElapsed = RealTimeElapsed;
+ double TimeElapsed = RealTimeElapsed;
int i;
+ UpdateTransition(TimeElapsed);
+
if (CurrMenu < 0 || CurrMenu >= Menus.Length)
+ {
+ DrawSidebarToggleButton(RealTimeElapsed);
return;
+ }
MenuBase menu = Menus[CurrMenu];
- // overlay background
- Renderer.Rectangle.Draw(null, Vector2.Null, new Vector2(Renderer.Screen.Width, Renderer.Screen.Height), overlayColor);
+ // overlay background (fades in/out with the sidebar)
+ Renderer.Rectangle.Draw(null, Vector2.Null, new Vector2(Renderer.Screen.Width, Renderer.Screen.Height), new Color128(overlayColor.R, overlayColor.G, overlayColor.B, (float)(overlayColor.A * SidebarFade)));
+
+ if (menu.Type == MenuType.Options)
+ {
+ Renderer.Rectangle.Draw(null, new Vector2(menuMin.X, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), new Color128(backgroundColor.R, backgroundColor.G, backgroundColor.B, (float)(backgroundColor.A * SidebarFade)));
+ if (OptionsTabContainer != null)
+ {
+ OptionsTabContainer.Draw();
+ LibRender2.Primitives.GLDropdown.ActiveDropdown?.DrawOverlay();
+ }
+ DrawSidebarToggleButton(RealTimeElapsed);
+ return;
+ }
double itemLeft, itemX;
if (menu.Align == TextAlignment.TopLeft)
{
- itemLeft = 0;
- itemX = 16;
- Renderer.Rectangle.Draw(null, new Vector2(0, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
+ itemLeft = menuMin.X;
+ itemX = menuMin.X + 16;
+ Renderer.Rectangle.Draw(null, new Vector2(menuMin.X, menuMin.Y - Border.Y), new Vector2(menuMax.X - menuMin.X + 2.0f * Border.X, menuMax.Y - menuMin.Y + 2.0f * Border.Y), backgroundColor);
}
else
{
@@ -394,8 +465,10 @@ public override void Draw(double RealTimeElapsed)
menu.Align, ColourNormal, false);
if (menu.Items[i] is MenuOption opt)
{
- Renderer.OpenGlString.Draw(MenuFont, opt.CurrentOption.ToString(), new Vector2((menuMax.X - menuMin.X + 2.0f * Border.X) + 4.0f, itemY),
- menu.Align, backgroundColor, false);
+ Color128 optColor = (i == menu.Selection) ? ColourHighlight : ColourNormal;
+ double optX = IsSidebarMode ? (menuMax.X - 120.0f) : ((menuMax.X - menuMin.X + 2.0f * Border.X) + 4.0f);
+ Renderer.OpenGlString.Draw(MenuFont, opt.DisplayValue, new Vector2(optX, itemY),
+ IsSidebarMode ? TextAlignment.TopLeft : menu.Align, optColor, false);
}
itemY += LineHeight;
if (menu.Items[i].Icon != null)
@@ -424,16 +497,19 @@ public override void Draw(double RealTimeElapsed)
if (menu.Selection == int.MaxValue && allowNextPhase) //HACK: Special value to make this work with minimum extra code
{
- Program.Renderer.Rectangle.Draw(null, new Vector2(Program.Renderer.Screen.Width - 200, Program.Renderer.Screen.Height - 40), new Vector2(190, 30), Color128.Black);
- Program.Renderer.Rectangle.Draw(null, new Vector2(Program.Renderer.Screen.Width - 197, Program.Renderer.Screen.Height - 37), new Vector2(184, 24), highlightColor);
- Program.Renderer.OpenGlString.Draw(MenuFont, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "start", "start_start" }), new Vector2(Program.Renderer.Screen.Width - 180, Program.Renderer.Screen.Height - 35), TextAlignment.TopLeft, Color128.Black);
+ Program.Renderer.Rectangle.Draw(null, new Vector2(menuMin.X + SidebarWidth - 200, Program.Renderer.Screen.Height - 40), new Vector2(190, 30), Color128.Black);
+ Program.Renderer.Rectangle.Draw(null, new Vector2(menuMin.X + SidebarWidth - 197, Program.Renderer.Screen.Height - 37), new Vector2(184, 24), highlightColor);
+ Program.Renderer.OpenGlString.Draw(MenuFont, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "start", "start_start" }), new Vector2(menuMin.X + SidebarWidth - 180, Program.Renderer.Screen.Height - 35), TextAlignment.TopLeft, Color128.Black);
}
else
{
- Program.Renderer.Rectangle.Draw(null, new Vector2(Program.Renderer.Screen.Width - 200, Program.Renderer.Screen.Height - 40), new Vector2(190, 30), Color128.Black);
- Program.Renderer.OpenGlString.Draw(MenuFont, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "start", "start_start" }), new Vector2(Program.Renderer.Screen.Width - 180, Program.Renderer.Screen.Height - 35), TextAlignment.TopLeft, Color128.Grey);
+ Program.Renderer.Rectangle.Draw(null, new Vector2(menuMin.X + SidebarWidth - 200, Program.Renderer.Screen.Height - 40), new Vector2(190, 30), Color128.Black);
+ Program.Renderer.OpenGlString.Draw(MenuFont, Translations.GetInterfaceString(HostApplication.OpenBve, new[] { "start", "start_start" }), new Vector2(menuMin.X + SidebarWidth - 180, Program.Renderer.Screen.Height - 35), TextAlignment.TopLeft, Color128.Grey);
}
}
+
+ DrawSidebarToggleButton(RealTimeElapsed);
+ DrawSidebarResizeGrip();
}
private static void routeWorkerThread_doWork(object sender, DoWorkEventArgs e)
diff --git a/source/RouteViewer/NewRendererR.cs b/source/RouteViewer/NewRendererR.cs
index a43d37c88..5a5c1d77a 100644
--- a/source/RouteViewer/NewRendererR.cs
+++ b/source/RouteViewer/NewRendererR.cs
@@ -764,7 +764,7 @@ private void RenderOverlays(double timeElapsed)
}
}
- if (CurrentInterface == InterfaceType.Menu)
+ if (CurrentInterface == InterfaceType.Menu || Game.Menu.IsSidebarMode)
{
Game.Menu.Draw(timeElapsed);
}
diff --git a/source/RouteViewer/Options.cs b/source/RouteViewer/Options.cs
index e368b0d7c..089e95a5f 100644
--- a/source/RouteViewer/Options.cs
+++ b/source/RouteViewer/Options.cs
@@ -12,9 +12,6 @@ namespace RouteViewer
/// Holds the program specific options
internal class Options : BaseOptions
{
- internal bool LoadingProgressBar;
- internal bool LoadingLogo;
- internal bool LoadingBackground;
internal string RouteSearchDirectory;
internal Options()
diff --git a/source/RouteViewer/ProgramR.cs b/source/RouteViewer/ProgramR.cs
index 5a280c7ce..b553e5512 100644
--- a/source/RouteViewer/ProgramR.cs
+++ b/source/RouteViewer/ProgramR.cs
@@ -338,6 +338,15 @@ internal static void MouseMoveEvent(object sender, MouseMoveEventArgs e)
internal static void MouseEvent(object sender, MouseButtonEventArgs e)
{
+ if (e.IsPressed && Game.Menu != null && Game.Menu.IsSidebarMode)
+ {
+ OpenBveApi.Math.Vector4 rect = Game.Menu.GetToggleButtonRect();
+ if (e.X >= rect.X && e.X <= rect.X + rect.Z && e.Y >= rect.Y && e.Y <= rect.Y + rect.W)
+ {
+ Game.Menu.ProcessMouseDown(e.X, e.Y);
+ return;
+ }
+ }
switch (Renderer.CurrentInterface)
{
case InterfaceType.Menu:
@@ -347,6 +356,10 @@ internal static void MouseEvent(object sender, MouseButtonEventArgs e)
// viewer hooks up and down to same event
Game.Menu.ProcessMouseDown(e.X, e.Y);
}
+ else
+ {
+ Game.Menu.ProcessMouseUp(e.X, e.Y);
+ }
break;
default:
if (e.Button == OpenTK.Input.MouseButton.Left)
@@ -423,6 +436,23 @@ internal static void FileDrop(object sender, FileDropEventArgs e)
internal static void KeyDownEvent(object sender, KeyboardKeyEventArgs e)
{
+ // Sidebar toggle must work whether the menu is open or not
+ if (e.Key == Key.O)
+ {
+ if (Game.Menu != null && Game.Menu.IsSidebarMode)
+ {
+ Game.Menu.ToggleSidebar();
+ }
+ return;
+ }
+ if (Renderer.CurrentInterface != InterfaceType.Normal)
+ {
+ Game.Menu.ProcessKeyDown(e.Key);
+ if (e.Key != Key.Up && e.Key != Key.Down && e.Key != Key.Enter && e.Key != Key.Escape && e.Key != Key.Left && e.Key != Key.Right)
+ {
+ return;
+ }
+ }
double speedModified = (ShiftPressed ? 2.0 : 1.0) * (ControlPressed ? 4.0 : 1.0) * (AltPressed ? 8.0 : 1.0);
switch (e.Key)
{
diff --git a/source/RouteViewer/System/Gamewindow.cs b/source/RouteViewer/System/Gamewindow.cs
index 81473826f..4b3f9ff25 100644
--- a/source/RouteViewer/System/Gamewindow.cs
+++ b/source/RouteViewer/System/Gamewindow.cs
@@ -92,6 +92,7 @@ protected override void OnResize(EventArgs e)
Program.Renderer.Screen.Width = Width;
Program.Renderer.Screen.Height = Height;
Program.Renderer.UpdateViewport(ViewportChangeMode.NoChange);
+ Game.Menu.ComputePosition();
}
protected override void OnLoad(EventArgs e)