From 9a4a480c263b576bb3091ccd744b8d53f622fe88 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:16:49 +0700 Subject: [PATCH 1/8] Improve in-game GUI: unified options, dynamic numeric input, real-time viewport updates, and modern slate/blue styling --- source/LibRender2/LibRender2.csproj | 11 + source/LibRender2/Menu/AbstractMenu.cs | 301 +++++++++++++++--- source/LibRender2/Menu/Menu.OptionType.cs | 16 +- .../LibRender2/Menu/MenuEntries/MenuOption.cs | 170 +++++++++- .../LibRender2/Primitives/GLBoxContainer.cs | 59 ++++ source/LibRender2/Primitives/GLContainer.cs | 50 +++ source/LibRender2/Primitives/GLDropdown.cs | 85 +++++ source/LibRender2/Primitives/GLLinkButton.cs | 44 +++ .../LibRender2/Primitives/GLNumericUpDown.cs | 59 ++++ source/LibRender2/Primitives/GLPanel.cs | 21 ++ source/LibRender2/Primitives/GLProgressBar.cs | 25 ++ .../Primitives/GLScrollContainer.cs | 39 +++ source/LibRender2/Primitives/GLSlider.cs | 56 ++++ source/LibRender2/Primitives/GLTextureRect.cs | 21 ++ source/LibRender2/Primitives/GLToggle.cs | 43 +++ source/ObjectViewer/Game/Menu.SingleMenu.cs | 20 +- source/ObjectViewer/Game/Menu.cs | 46 ++- source/ObjectViewer/Graphics/NewRendererS.cs | 2 +- source/ObjectViewer/ProgramS.cs | 18 +- source/OpenBVE/Game/Menu/Menu.SingleMenu.cs | 29 +- source/OpenBVE/Game/Menu/Menu.cs | 55 +++- source/OpenBVE/Graphics/NewRenderer.cs | 14 +- source/OpenBVE/System/Input/Keyboard.cs | 17 +- source/OpenBVE/System/MainLoop.cs | 9 + source/OpenBveApi/System/BaseOptions.cs | 6 + source/RouteViewer/Game/Menu.SingleMenu.cs | 20 +- source/RouteViewer/Game/Menu.cs | 69 +++- source/RouteViewer/NewRendererR.cs | 2 +- source/RouteViewer/Options.cs | 3 - source/RouteViewer/ProgramR.cs | 17 + 30 files changed, 1172 insertions(+), 155 deletions(-) create mode 100644 source/LibRender2/Primitives/GLBoxContainer.cs create mode 100644 source/LibRender2/Primitives/GLContainer.cs create mode 100644 source/LibRender2/Primitives/GLDropdown.cs create mode 100644 source/LibRender2/Primitives/GLLinkButton.cs create mode 100644 source/LibRender2/Primitives/GLNumericUpDown.cs create mode 100644 source/LibRender2/Primitives/GLPanel.cs create mode 100644 source/LibRender2/Primitives/GLProgressBar.cs create mode 100644 source/LibRender2/Primitives/GLScrollContainer.cs create mode 100644 source/LibRender2/Primitives/GLSlider.cs create mode 100644 source/LibRender2/Primitives/GLTextureRect.cs create mode 100644 source/LibRender2/Primitives/GLToggle.cs diff --git a/source/LibRender2/LibRender2.csproj b/source/LibRender2/LibRender2.csproj index dcea6d70e..82724fa68 100644 --- a/source/LibRender2/LibRender2.csproj +++ b/source/LibRender2/LibRender2.csproj @@ -100,6 +100,17 @@ + + + + + + + + + + + diff --git a/source/LibRender2/Menu/AbstractMenu.cs b/source/LibRender2/Menu/AbstractMenu.cs index c65cb5292..e3d5b2ba9 100644 --- a/source/LibRender2/Menu/AbstractMenu.cs +++ b/source/LibRender2/Menu/AbstractMenu.cs @@ -32,6 +32,7 @@ using OpenBveApi; using OpenBveApi.Input; using OpenBveApi.Interface; +using OpenBveApi.Hosts; namespace LibRender2.Menu { @@ -39,13 +40,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 +55,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; @@ -155,6 +156,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; + } } } @@ -170,6 +178,75 @@ public virtual void ProcessMouseScroll(int Scroll) 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 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); + } + + 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; + + if (x >= 1.0) + { + currentOffset = target; + isAnimating = false; + if (!SidebarVisible) + { + Reset(); + } + } + } + else + { + currentOffset = target; + } + + menuMin.X = currentOffset; + menuMax.X = menuMin.X + SidebarWidth; + + RepositionSidebarControls(); + } + + public virtual void RepositionSidebarControls() + { + } + /// Processes a mouse move event /// The screen-relative x coordinate of the move event /// The screen-relative y coordinate of the move event @@ -183,6 +260,35 @@ public virtual bool ProcessMouseMove(int x, int y) /// The screen-relative y coordinate of the down event public void ProcessMouseDown(int x, int y) { + if (IsSidebarMode) + { + Vector4 rect = GetToggleButtonRect(); + if (x >= rect.X && x <= rect.X + rect.Z && y >= rect.Y && y <= rect.Y + rect.W) + { + 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(); + return; + } + } + for (int i = 0; i < menuControls.Count; i++) { if (menuControls[i].IsVisible) @@ -220,7 +326,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() @@ -241,44 +346,157 @@ public void ComputePosition() } } - // HORIZONTAL PLACEMENT - switch (menu.Align) + if (IsSidebarMode) + { + 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" })); + } + + 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) { - 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; + 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" })); } - 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) + 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 (CurrMenu < 0 || Menus.Length == 0) return; + var menu = Menus[CurrMenu]; + if (menu.Selection >= 0 && menu.Selection < menu.Items.Length) { - // 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; + var selectedEntry = menu.Items[menu.Selection]; + if (selectedEntry is MenuOption opt) + { + opt.ProcessKeyDown(key); + } } } @@ -286,6 +504,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..3bc704fe0 100644 --- a/source/LibRender2/Menu/MenuEntries/MenuOption.cs +++ b/source/LibRender2/Menu/MenuEntries/MenuOption.cs @@ -23,12 +23,14 @@ //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 { @@ -40,9 +42,27 @@ public class MenuOption : MenuEntry 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++; @@ -280,6 +332,12 @@ public void Flip() 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 +380,119 @@ 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); + } + } + } } } diff --git a/source/LibRender2/Primitives/GLBoxContainer.cs b/source/LibRender2/Primitives/GLBoxContainer.cs new file mode 100644 index 000000000..2b7e02a14 --- /dev/null +++ b/source/LibRender2/Primitives/GLBoxContainer.cs @@ -0,0 +1,59 @@ +using OpenBveApi.Math; + +namespace LibRender2.Primitives +{ + public enum BoxDirection + { + Horizontal, + Vertical + } + + public class GLBoxContainer : GLContainer + { + public BoxDirection Direction = BoxDirection.Vertical; + public float Spacing = 4f; + + public GLBoxContainer(BaseRenderer renderer) : base(renderer) + { + IsVisible = true; + } + + public override void Draw() + { + if (!IsVisible) return; + // Update child positions based on direction and spacing + double offset = 0.0; + for (int i = 0; i < Children.Count; i++) + { + if (!Children[i].IsVisible) continue; + if (Direction == BoxDirection.Vertical) + { + Children[i].Location = new Vector2(Location.X, Location.Y + offset); + offset += Children[i].Size.Y + Spacing; + } + else + { + Children[i].Location = new Vector2(Location.X + offset, Location.Y); + 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/GLContainer.cs b/source/LibRender2/Primitives/GLContainer.cs new file mode 100644 index 000000000..16bcbdf4e --- /dev/null +++ b/source/LibRender2/Primitives/GLContainer.cs @@ -0,0 +1,50 @@ +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) + { + } + + public override void Draw() + { + if (!IsVisible) return; + for (int i = 0; i < Children.Count; i++) + { + if (Children[i].IsVisible) + { + 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); + } + } + } + } +} diff --git a/source/LibRender2/Primitives/GLDropdown.cs b/source/LibRender2/Primitives/GLDropdown.cs new file mode 100644 index 000000000..c2e8d6d4d --- /dev/null +++ b/source/LibRender2/Primitives/GLDropdown.cs @@ -0,0 +1,85 @@ +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; + + public GLDropdown(BaseRenderer renderer) : base(renderer) + { + IsVisible = true; + Size = new Vector2(160, 24); + BackgroundColor = new Color128(0.15f, 0.15f, 0.15f, 1f); + } + + public override void Draw() + { + if (!IsVisible) return; + Renderer.Rectangle.Draw(null, Location, Size, BackgroundColor); + Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, "v", new Vector2(Location.X + Size.X - 20, Location.Y + 2), TextAlignment.TopLeft, 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); + } + + if (Expanded) + { + float itemHeight = 24f; + float overlayHeight = Items.Count * itemHeight; + Vector2 overlayLoc = new Vector2(Location.X, Location.Y + Size.Y); + Renderer.Rectangle.Draw(null, overlayLoc, new Vector2(Size.X, overlayHeight), new Color128(0.1f, 0.1f, 0.1f, 0.95f)); + + for (int i = 0; i < Items.Count; i++) + { + Vector2 itemLoc = 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); + } + Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Items[i], new Vector2(itemLoc.X + 8, itemLoc.Y + 2), TextAlignment.TopLeft, Color128.White); + } + } + } + + public override void MouseDown(int x, int y) + { + if (!IsVisible) return; + if (Expanded) + { + float itemHeight = 24f; + float overlayHeight = Items.Count * itemHeight; + Vector2 overlayLoc = 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; + return; + } + } + + if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y) + { + Expanded = !Expanded; + } + else + { + Expanded = false; + } + } + } +} diff --git a/source/LibRender2/Primitives/GLLinkButton.cs b/source/LibRender2/Primitives/GLLinkButton.cs new file mode 100644 index 000000000..041b2b1e5 --- /dev/null +++ b/source/LibRender2/Primitives/GLLinkButton.cs @@ -0,0 +1,44 @@ +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 GLLinkButton(BaseRenderer renderer) : base(renderer) + { + IsVisible = true; + Size = new Vector2(100, 20); + } + + public override void Draw() + { + if (!IsVisible) return; + Color128 color = CurrentlySelected ? HoverColor : NormalColor; + 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) + { + OnClick?.Invoke(this, EventArgs.Empty); + } + } + } +} diff --git a/source/LibRender2/Primitives/GLNumericUpDown.cs b/source/LibRender2/Primitives/GLNumericUpDown.cs new file mode 100644 index 000000000..30c35df5b --- /dev/null +++ b/source/LibRender2/Primitives/GLNumericUpDown.cs @@ -0,0 +1,59 @@ +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; + + 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; + Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X - 30, Size.Y), BackgroundColor); + Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Value.ToString("0.##"), new Vector2(Location.X + 8, Location.Y + 2), TextAlignment.TopLeft, Color128.White); + + Vector2 upLoc = new Vector2(Location.X + Size.X - 28, Location.Y); + Vector2 downLoc = new Vector2(Location.X + Size.X - 14, Location.Y); + + Renderer.Rectangle.Draw(null, upLoc, new Vector2(12, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); + Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, "+", new Vector2(upLoc.X + 2, upLoc.Y + 2), TextAlignment.TopLeft, Color128.White); + + Renderer.Rectangle.Draw(null, downLoc, new Vector2(12, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); + Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, "-", new Vector2(downLoc.X + 2, downLoc.Y + 2), TextAlignment.TopLeft, Color128.White); + } + + public override void MouseDown(int x, int y) + { + if (!IsVisible) return; + Vector2 upLoc = new Vector2(Location.X + Size.X - 28, Location.Y); + Vector2 downLoc = new Vector2(Location.X + Size.X - 14, Location.Y); + + if (x >= upLoc.X && x <= upLoc.X + 12 && y >= upLoc.Y && y <= upLoc.Y + Size.Y) + { + 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 + 12 && y >= downLoc.Y && y <= downLoc.Y + Size.Y) + { + Value = System.Math.Max(Minimum, Value - Increment); + ValueChanged?.Invoke(this, EventArgs.Empty); + OnClick?.Invoke(this, EventArgs.Empty); + } + } + } +} 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..a8d0f3edf --- /dev/null +++ b/source/LibRender2/Primitives/GLProgressBar.cs @@ -0,0 +1,25 @@ +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 GLProgressBar(BaseRenderer renderer) : base(renderer) + { + IsVisible = true; + BackgroundColor = new Color128(0.2f, 0.2f, 0.2f, 1f); + } + + public override void Draw() + { + if (!IsVisible) return; + Renderer.Rectangle.Draw(null, Location, Size, BackgroundColor); + double fillWidth = Size.X * System.Math.Max(0.0, System.Math.Min(1.0, Value)); + 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..5c4f0d7be --- /dev/null +++ b/source/LibRender2/Primitives/GLScrollContainer.cs @@ -0,0 +1,39 @@ +using OpenBveApi.Math; + +namespace LibRender2.Primitives +{ + public class GLScrollContainer : GLContainer + { + public double ScrollOffset = 0.0; + public double MaxScroll = 0.0; + + public GLScrollContainer(BaseRenderer renderer) : base(renderer) + { + IsVisible = 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; + Vector2 originalLocation = Children[i].Location; + Children[i].Location = new Vector2(originalLocation.X, originalLocation.Y - ScrollOffset); + if (Children[i].Location.Y >= Location.Y && Children[i].Location.Y + Children[i].Size.Y <= Location.Y + Size.Y) + { + Children[i].Draw(); + } + Children[i].Location = originalLocation; + totalHeight += Children[i].Size.Y + 4.0; + } + MaxScroll = System.Math.Max(0.0, totalHeight - Size.Y); + } + + 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..d9b9b599c --- /dev/null +++ b/source/LibRender2/Primitives/GLSlider.cs @@ -0,0 +1,56 @@ +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; + Renderer.Rectangle.Draw(null, new Vector2(Location.X, centerY), new Vector2(Size.X, 4.0), BackgroundColor); + + double ratio = (Value - Minimum) / (Maximum - Minimum); + double knobX = Location.X + ratio * (Size.X - 12.0); + Renderer.Rectangle.Draw(null, new Vector2(knobX, Location.Y + 2.0), new Vector2(12.0, Size.Y - 4.0), Color128.Orange); + } + + public override void MouseMove(int x, int y) + { + if (!IsVisible) return; + if (dragging) + { + double localX = System.Math.Max(0.0, System.Math.Min(Size.X - 12.0, x - Location.X - 6.0)); + double ratio = localX / (Size.X - 12.0); + Value = Minimum + ratio * (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; + MouseMove(x, y); + } + } + } +} 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..8fc9c752e --- /dev/null +++ b/source/LibRender2/Primitives/GLToggle.cs @@ -0,0 +1,43 @@ +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; + + 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; + Renderer.Rectangle.Draw(null, Location, new Vector2(20, 20), BackgroundColor); + if (Checked) + { + Renderer.Rectangle.Draw(null, new Vector2(Location.X + 4, Location.Y + 4), new Vector2(12, 12), Color128.Orange); + } + Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Text, new Vector2(Location.X + 28, Location.Y + 2), TextAlignment.TopLeft, Color128.White); + } + + 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) + { + Checked = !Checked; + ValueChanged?.Invoke(this, EventArgs.Empty); + OnClick?.Invoke(this, EventArgs.Empty); + } + } + } +} 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..78c13722e 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,23 @@ public override void Initialize() filePictureBox.Size = new Vector2(quarterWidth, quarterWidth); filePictureBox.BackgroundColor = Color128.White; SearchDirectory = Interface.CurrentOptions.ObjectSearchDirectory; + + menuControls.Add(filePictureBox); + menuControls.Add(fileTextBox); IsInitialized = true; } + public override void RepositionSidebarControls() + { + if (!IsInitialized) return; + 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 +105,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; } @@ -268,12 +288,16 @@ 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 @@ -283,9 +307,9 @@ public override void Draw(double RealTimeElapsed) 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 +393,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 +416,8 @@ 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); } } } diff --git a/source/ObjectViewer/Graphics/NewRendererS.cs b/source/ObjectViewer/Graphics/NewRendererS.cs index 86d1b1b41..e8a01c1a1 100644 --- a/source/ObjectViewer/Graphics/NewRendererS.cs +++ b/source/ObjectViewer/Graphics/NewRendererS.cs @@ -423,7 +423,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 9d53fb7cc..a98fbc0ff 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: @@ -538,7 +547,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/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..d2346f869 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(); @@ -100,6 +112,19 @@ public override void Initialize() menuControls.Add(nextStepButton); } + public override void RepositionSidebarControls() + { + if (!IsInitialized) return; + 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 internal void OnResize() { @@ -207,6 +232,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; } @@ -750,12 +782,18 @@ 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 @@ -765,9 +803,9 @@ public override void Draw(double RealTimeElapsed) 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 +889,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 +1039,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 7ae9dca3e..1bfefd7d9 100644 --- a/source/OpenBVE/Graphics/NewRenderer.cs +++ b/source/OpenBVE/Graphics/NewRenderer.cs @@ -508,15 +508,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 5bc384c7b..5978b5b13 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: 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..011066d13 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,22 @@ public override void Initialize() routePictureBox.BackgroundColor = Color128.White; SearchDirectory = Interface.CurrentOptions.RouteSearchDirectory; + menuControls.Add(routePictureBox); + menuControls.Add(routeDescriptionBox); IsInitialized = true; } + public override void RepositionSidebarControls() + { + if (!IsInitialized) return; + 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 +125,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; } @@ -265,7 +296,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,12 +324,16 @@ 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 @@ -308,9 +343,9 @@ public override void Draw(double RealTimeElapsed) 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 +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) @@ -424,16 +461,18 @@ 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); } private static void routeWorkerThread_doWork(object sender, DoWorkEventArgs e) diff --git a/source/RouteViewer/NewRendererR.cs b/source/RouteViewer/NewRendererR.cs index 7a54a7115..138022a21 100644 --- a/source/RouteViewer/NewRendererR.cs +++ b/source/RouteViewer/NewRendererR.cs @@ -795,7 +795,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 defcfd3b4..f5b8fe827 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 b718bd574..a01f15b6a 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: @@ -423,6 +432,14 @@ internal static void FileDrop(object sender, FileDropEventArgs e) internal static void KeyDownEvent(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; + } + } double speedModified = (ShiftPressed ? 2.0 : 1.0) * (ControlPressed ? 4.0 : 1.0) * (AltPressed ? 8.0 : 1.0); switch (e.Key) { From cc83851c1ab07b35d77139f2781057e985f8ae54 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:23:38 +0700 Subject: [PATCH 2/8] Support instant texture updates when changing interpolation or anisotropic level --- source/LibRender2/Menu/MenuEntries/MenuOption.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/LibRender2/Menu/MenuEntries/MenuOption.cs b/source/LibRender2/Menu/MenuEntries/MenuOption.cs index 3bc704fe0..4b34b5b17 100644 --- a/source/LibRender2/Menu/MenuEntries/MenuOption.cs +++ b/source/LibRender2/Menu/MenuEntries/MenuOption.cs @@ -319,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; @@ -326,6 +328,8 @@ 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); From 5fd24b15dc44b70409eb2632c30e6927a0c308df Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:32:11 +0700 Subject: [PATCH 3/8] Implement manual input and Blender-style horizontal mouse dragging/scrubbing for numerical settings --- source/LibRender2/Menu/AbstractMenu.cs | 31 ++++++++++++++++++- .../LibRender2/Menu/MenuEntries/MenuOption.cs | 31 ++++++++++++++++++- source/ObjectViewer/Game/Menu.cs | 10 ++++++ source/ObjectViewer/ProgramS.cs | 4 +++ source/OpenBVE/Game/Menu/Menu.cs | 10 ++++++ source/OpenBVE/System/MainLoop.cs | 4 +++ source/RouteViewer/Game/Menu.cs | 10 ++++++ source/RouteViewer/ProgramR.cs | 4 +++ 8 files changed, 102 insertions(+), 2 deletions(-) diff --git a/source/LibRender2/Menu/AbstractMenu.cs b/source/LibRender2/Menu/AbstractMenu.cs index e3d5b2ba9..736fee575 100644 --- a/source/LibRender2/Menu/AbstractMenu.cs +++ b/source/LibRender2/Menu/AbstractMenu.cs @@ -196,6 +196,10 @@ public virtual void ProcessMouseScroll(int Scroll) 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; @@ -288,6 +292,20 @@ public void ProcessMouseDown(int x, int 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++) { @@ -308,10 +326,21 @@ 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 + public void ProcessMouseUp(int x, int y) + { + isScrubbing = false; + scrubbingOption = null; + } + /// Processes a user command for the current menu /// The command to apply to the current menu /// The time elapsed since previous frame diff --git a/source/LibRender2/Menu/MenuEntries/MenuOption.cs b/source/LibRender2/Menu/MenuEntries/MenuOption.cs index 4b34b5b17..1b16fa0d7 100644 --- a/source/LibRender2/Menu/MenuEntries/MenuOption.cs +++ b/source/LibRender2/Menu/MenuEntries/MenuOption.cs @@ -36,7 +36,7 @@ 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; @@ -498,5 +498,34 @@ private void ApplyCustomValue() } } } + + 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/ObjectViewer/Game/Menu.cs b/source/ObjectViewer/Game/Menu.cs index 78c13722e..d0c007d0a 100644 --- a/source/ObjectViewer/Game/Menu.cs +++ b/source/ObjectViewer/Game/Menu.cs @@ -245,6 +245,16 @@ public override bool ProcessMouseMove(int x, int y) { return false; } + 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; diff --git a/source/ObjectViewer/ProgramS.cs b/source/ObjectViewer/ProgramS.cs index a98fbc0ff..a76daa80e 100644 --- a/source/ObjectViewer/ProgramS.cs +++ b/source/ObjectViewer/ProgramS.cs @@ -240,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; diff --git a/source/OpenBVE/Game/Menu/Menu.cs b/source/OpenBVE/Game/Menu/Menu.cs index d2346f869..75c1e0ab0 100644 --- a/source/OpenBVE/Game/Menu/Menu.cs +++ b/source/OpenBVE/Game/Menu/Menu.cs @@ -330,6 +330,16 @@ public override bool ProcessMouseMove(int x, int y) { return false; } + 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) diff --git a/source/OpenBVE/System/MainLoop.cs b/source/OpenBVE/System/MainLoop.cs index 5978b5b13..4c4b51577 100644 --- a/source/OpenBVE/System/MainLoop.cs +++ b/source/OpenBVE/System/MainLoop.cs @@ -187,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/RouteViewer/Game/Menu.cs b/source/RouteViewer/Game/Menu.cs index 011066d13..919e3c98d 100644 --- a/source/RouteViewer/Game/Menu.cs +++ b/source/RouteViewer/Game/Menu.cs @@ -280,6 +280,16 @@ public override bool ProcessMouseMove(int x, int y) { return false; } + 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; diff --git a/source/RouteViewer/ProgramR.cs b/source/RouteViewer/ProgramR.cs index a01f15b6a..6dbef88a3 100644 --- a/source/RouteViewer/ProgramR.cs +++ b/source/RouteViewer/ProgramR.cs @@ -356,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) From affeeea0bfbbad0aaa4ddd0999e2cd9aaaeab22a Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:16:19 +0700 Subject: [PATCH 4/8] Implement tabbed settings interface component with GLTabContainer --- source/LibRender2/LibRender2.csproj | 1 + source/LibRender2/Menu/AbstractMenu.cs | 267 ++++++++++++++++++ .../LibRender2/Primitives/GLTabContainer.cs | 95 +++++++ source/ObjectViewer/Game/Menu.cs | 21 ++ source/OpenBVE/Game/Menu/Menu.cs | 23 ++ source/RouteViewer/Game/Menu.cs | 21 ++ 6 files changed, 428 insertions(+) create mode 100644 source/LibRender2/Primitives/GLTabContainer.cs diff --git a/source/LibRender2/LibRender2.csproj b/source/LibRender2/LibRender2.csproj index 82724fa68..fd48525f9 100644 --- a/source/LibRender2/LibRender2.csproj +++ b/source/LibRender2/LibRender2.csproj @@ -111,6 +111,7 @@ + diff --git a/source/LibRender2/Menu/AbstractMenu.cs b/source/LibRender2/Menu/AbstractMenu.cs index 736fee575..06ec485b6 100644 --- a/source/LibRender2/Menu/AbstractMenu.cs +++ b/source/LibRender2/Menu/AbstractMenu.cs @@ -33,6 +33,8 @@ using OpenBveApi.Input; using OpenBveApi.Interface; using OpenBveApi.Hosts; +using LibRender2.Viewports; +using OpenBveApi.Objects; namespace LibRender2.Menu { @@ -108,6 +110,254 @@ 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 = 16f }; + row.Size = new Vector2(300f, 24f); + var label = new Label(Renderer, name); + label.BackgroundColor = new Color128(0, 0, 0, 0); + label.Size = new Vector2(140f, 24f); + row.Children.Add(label); + return row; + } + /// Whether the menu system is initialized public bool IsInitialized = false; @@ -249,6 +499,11 @@ public void UpdateTransition(double TimeElapsed) public virtual void RepositionSidebarControls() { + if (OptionsTabContainer != null) + { + OptionsTabContainer.Location = new Vector2((float)menuMin.X + 16f, 50f); + OptionsTabContainer.Size = new Vector2((float)SidebarWidth - 32f, (float)Renderer.Screen.Height - 100f); + } } /// Processes a mouse move event @@ -292,6 +547,14 @@ public void ProcessMouseDown(int x, int y) 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]; @@ -363,6 +626,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++) { /* diff --git a/source/LibRender2/Primitives/GLTabContainer.cs b/source/LibRender2/Primitives/GLTabContainer.cs new file mode 100644 index 000000000..e59964f0e --- /dev/null +++ b/source/LibRender2/Primitives/GLTabContainer.cs @@ -0,0 +1,95 @@ +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 GLTabContainer(BaseRenderer renderer) : base(renderer) + { + IsVisible = 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); + Color128 headerBg = isSelected ? new Color128(0.2f, 0.22f, 0.25f, 0.9f) : 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); + } + + // 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 = new Vector2(Location.X, Location.Y + TabHeaderHeight); + TabContents[i].Size = new Vector2(Size.X, Size.Y - TabHeaderHeight); + TabContents[i].Draw(); + } + } + } + + 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); + } + } +} diff --git a/source/ObjectViewer/Game/Menu.cs b/source/ObjectViewer/Game/Menu.cs index d0c007d0a..9b893f832 100644 --- a/source/ObjectViewer/Game/Menu.cs +++ b/source/ObjectViewer/Game/Menu.cs @@ -64,12 +64,14 @@ public override void Initialize() 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); @@ -245,6 +247,14 @@ 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; @@ -313,6 +323,17 @@ public override void Draw(double RealTimeElapsed) // 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(); + } + DrawSidebarToggleButton(RealTimeElapsed); + return; + } + double itemLeft, itemX; if (menu.Align == TextAlignment.TopLeft) diff --git a/source/OpenBVE/Game/Menu/Menu.cs b/source/OpenBVE/Game/Menu/Menu.cs index 75c1e0ab0..76b1c0728 100644 --- a/source/OpenBVE/Game/Menu/Menu.cs +++ b/source/OpenBVE/Game/Menu/Menu.cs @@ -110,11 +110,13 @@ 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); @@ -330,6 +332,14 @@ 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; @@ -809,6 +819,19 @@ public override void Draw(double RealTimeElapsed) // 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(); + } + DrawSidebarToggleButton(RealTimeElapsed); + Renderer.PopMatrix(MatrixMode.Modelview); + Renderer.PopMatrix(MatrixMode.Projection); + return; + } + double itemLeft, itemX; if (menu.Align == TextAlignment.TopLeft) diff --git a/source/RouteViewer/Game/Menu.cs b/source/RouteViewer/Game/Menu.cs index 919e3c98d..2997549e7 100644 --- a/source/RouteViewer/Game/Menu.cs +++ b/source/RouteViewer/Game/Menu.cs @@ -84,12 +84,14 @@ public override void Initialize() 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); @@ -280,6 +282,14 @@ 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; @@ -349,6 +359,17 @@ public override void Draw(double RealTimeElapsed) // 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(); + } + DrawSidebarToggleButton(RealTimeElapsed); + return; + } + double itemLeft, itemX; if (menu.Align == TextAlignment.TopLeft) From 4012d5d249fc635e0213c13ccd2961c8bf5a4a44 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:13:04 +0700 Subject: [PATCH 5/8] Fix settings GUI labels visibility, improve dropdown/numeric control aesthetics and auto-scaling layout --- source/LibRender2/Menu/AbstractMenu.cs | 25 +++++++++++++++- source/LibRender2/Primitives/GLDropdown.cs | 11 ++++++- .../LibRender2/Primitives/GLNumericUpDown.cs | 29 ++++++++++++------- source/LibRender2/Primitives/Label.cs | 3 +- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/source/LibRender2/Menu/AbstractMenu.cs b/source/LibRender2/Menu/AbstractMenu.cs index 06ec485b6..afb579182 100644 --- a/source/LibRender2/Menu/AbstractMenu.cs +++ b/source/LibRender2/Menu/AbstractMenu.cs @@ -502,7 +502,30 @@ public virtual void RepositionSidebarControls() if (OptionsTabContainer != null) { OptionsTabContainer.Location = new Vector2((float)menuMin.X + 16f, 50f); - OptionsTabContainer.Size = new Vector2((float)SidebarWidth - 32f, (float)Renderer.Screen.Height - 100f); + 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.Size = new Vector2(containerWidth, row.Size.Y); + if (row.Children.Count >= 2) + { + var label = row.Children[0]; + var control = row.Children[1]; + label.Size = new Vector2(containerWidth * 0.45f, row.Size.Y); + control.Size = new Vector2(containerWidth * 0.5f, row.Size.Y); + } + } + } + } + } } } diff --git a/source/LibRender2/Primitives/GLDropdown.cs b/source/LibRender2/Primitives/GLDropdown.cs index c2e8d6d4d..f100c91e1 100644 --- a/source/LibRender2/Primitives/GLDropdown.cs +++ b/source/LibRender2/Primitives/GLDropdown.cs @@ -24,7 +24,16 @@ public override void Draw() { if (!IsVisible) return; Renderer.Rectangle.Draw(null, Location, Size, BackgroundColor); - Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, "v", new Vector2(Location.X + Size.X - 20, Location.Y + 2), TextAlignment.TopLeft, Color128.White); + + // Draw dropdown arrow + float arrowX = (float)(Location.X + Size.X - 18); + float arrowY = (float)(Location.Y + (Size.Y - 5) / 2); + 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); diff --git a/source/LibRender2/Primitives/GLNumericUpDown.cs b/source/LibRender2/Primitives/GLNumericUpDown.cs index 30c35df5b..255331cc8 100644 --- a/source/LibRender2/Primitives/GLNumericUpDown.cs +++ b/source/LibRender2/Primitives/GLNumericUpDown.cs @@ -23,32 +23,39 @@ public GLNumericUpDown(BaseRenderer renderer) : base(renderer) public override void Draw() { if (!IsVisible) return; - Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X - 30, Size.Y), BackgroundColor); + Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X - 34, Size.Y), BackgroundColor); Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Value.ToString("0.##"), new Vector2(Location.X + 8, Location.Y + 2), TextAlignment.TopLeft, Color128.White); - Vector2 upLoc = new Vector2(Location.X + Size.X - 28, Location.Y); - Vector2 downLoc = new Vector2(Location.X + Size.X - 14, Location.Y); + Vector2 upLoc = new Vector2(Location.X + Size.X - 32, Location.Y); + Vector2 downLoc = new Vector2(Location.X + Size.X - 16, Location.Y); - Renderer.Rectangle.Draw(null, upLoc, new Vector2(12, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); - Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, "+", new Vector2(upLoc.X + 2, upLoc.Y + 2), TextAlignment.TopLeft, Color128.White); + // Draw Up (+) button + Renderer.Rectangle.Draw(null, upLoc, new Vector2(15, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); + 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); - Renderer.Rectangle.Draw(null, downLoc, new Vector2(12, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); - Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, "-", new Vector2(downLoc.X + 2, downLoc.Y + 2), TextAlignment.TopLeft, Color128.White); + // Draw Down (-) button + Renderer.Rectangle.Draw(null, downLoc, new Vector2(15, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); + 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 MouseDown(int x, int y) { if (!IsVisible) return; - Vector2 upLoc = new Vector2(Location.X + Size.X - 28, Location.Y); - Vector2 downLoc = new Vector2(Location.X + Size.X - 14, Location.Y); + 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 + 12 && y >= upLoc.Y && y <= upLoc.Y + Size.Y) + if (x >= upLoc.X && x <= upLoc.X + 15 && y >= upLoc.Y && y <= upLoc.Y + Size.Y) { 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 + 12 && y >= downLoc.Y && y <= downLoc.Y + Size.Y) + else if (x >= downLoc.X && x <= downLoc.X + 15 && y >= downLoc.Y && y <= downLoc.Y + Size.Y) { Value = System.Math.Max(Minimum, Value - Increment); ValueChanged?.Invoke(this, EventArgs.Empty); diff --git a/source/LibRender2/Primitives/Label.cs b/source/LibRender2/Primitives/Label.cs index 710fbc4a6..e8648c98a 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 // @@ -45,6 +45,7 @@ public Label(BaseRenderer renderer, string text) : base(renderer) // default colors to match GLMenu BackgroundColor = Color128.Black; TextColor = Color128.White; + IsVisible = true; } public override void Draw() From c1feae2e35eecdc256468fa161744a9ade6ab1b0 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:13:25 +0700 Subject: [PATCH 6/8] Fix dropdown stacking order and implement manual numeric text input typing support --- source/LibRender2/LibRender2.csproj | 4 +- source/LibRender2/Menu/AbstractMenu.cs | 25 ++ .../LibRender2/Primitives/GLBoxContainer.cs | 100 +++---- source/LibRender2/Primitives/GLButton.cs | 103 +++++++ source/LibRender2/Primitives/GLContainer.cs | 123 ++++++--- source/LibRender2/Primitives/GLControl.cs | 106 ++++--- source/LibRender2/Primitives/GLDropdown.cs | 260 ++++++++++++------ source/LibRender2/Primitives/GLLabel.cs | 40 +++ source/LibRender2/Primitives/GLLinkButton.cs | 84 +++--- .../LibRender2/Primitives/GLNumericUpDown.cs | 238 ++++++++++++---- source/LibRender2/Primitives/GLProgressBar.cs | 41 +-- .../Primitives/GLScrollContainer.cs | 92 ++++--- source/LibRender2/Primitives/GLSlider.cs | 101 ++++--- .../LibRender2/Primitives/GLTabContainer.cs | 225 +++++++++------ source/LibRender2/Primitives/GLToggle.cs | 103 ++++--- source/ObjectViewer/Game/Menu.cs | 1 + source/OpenBVE/Game/Menu/Menu.cs | 1 + source/RouteViewer/Game/Menu.cs | 1 + 18 files changed, 1126 insertions(+), 522 deletions(-) create mode 100644 source/LibRender2/Primitives/GLButton.cs create mode 100644 source/LibRender2/Primitives/GLLabel.cs diff --git a/source/LibRender2/LibRender2.csproj b/source/LibRender2/LibRender2.csproj index fd48525f9..c47eef0ee 100644 --- a/source/LibRender2/LibRender2.csproj +++ b/source/LibRender2/LibRender2.csproj @@ -103,6 +103,8 @@ + + @@ -154,7 +156,7 @@ - + diff --git a/source/LibRender2/Menu/AbstractMenu.cs b/source/LibRender2/Menu/AbstractMenu.cs index afb579182..78b3379b9 100644 --- a/source/LibRender2/Menu/AbstractMenu.cs +++ b/source/LibRender2/Menu/AbstractMenu.cs @@ -424,6 +424,11 @@ 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); } @@ -625,6 +630,21 @@ public void ProcessMouseUp(int x, int y) { isScrubbing = false; scrubbingOption = null; + 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); + } + } } /// Processes a user command for the current menu @@ -807,6 +827,11 @@ public virtual void OnOptionChanged(OptionType type) /// 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) diff --git a/source/LibRender2/Primitives/GLBoxContainer.cs b/source/LibRender2/Primitives/GLBoxContainer.cs index 2b7e02a14..8393a6b0c 100644 --- a/source/LibRender2/Primitives/GLBoxContainer.cs +++ b/source/LibRender2/Primitives/GLBoxContainer.cs @@ -2,58 +2,60 @@ namespace LibRender2.Primitives { - public enum BoxDirection - { - Horizontal, - Vertical - } + public enum BoxDirection + { + Horizontal, + Vertical + } - public class GLBoxContainer : GLContainer - { - public BoxDirection Direction = BoxDirection.Vertical; - public float Spacing = 4f; + 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 GLBoxContainer(BaseRenderer renderer) : base(renderer) + { + IsVisible = true; + } - public override void Draw() - { - if (!IsVisible) return; - // Update child positions based on direction and spacing - double offset = 0.0; - for (int i = 0; i < Children.Count; i++) - { - if (!Children[i].IsVisible) continue; - if (Direction == BoxDirection.Vertical) - { - Children[i].Location = new Vector2(Location.X, Location.Y + offset); - offset += Children[i].Size.Y + Spacing; - } - else - { - Children[i].Location = new Vector2(Location.X + offset, Location.Y); - offset += Children[i].Size.X + Spacing; - } - } - base.Draw(); - } - } + 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 GLHBoxContainer : GLBoxContainer + { + public GLHBoxContainer(BaseRenderer renderer) : base(renderer) + { + Direction = BoxDirection.Horizontal; + } + } - public class GLVBoxContainer : GLBoxContainer - { - public GLVBoxContainer(BaseRenderer renderer) : base(renderer) - { - Direction = BoxDirection.Vertical; - } - } + 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 index 16bcbdf4e..8547f79c7 100644 --- a/source/LibRender2/Primitives/GLContainer.cs +++ b/source/LibRender2/Primitives/GLContainer.cs @@ -3,48 +3,83 @@ namespace LibRender2.Primitives { - public abstract class GLContainer : GLControl - { - public readonly List Children = new List(); - - protected GLContainer(BaseRenderer renderer) : base(renderer) - { - } - - public override void Draw() - { - if (!IsVisible) return; - for (int i = 0; i < Children.Count; i++) - { - if (Children[i].IsVisible) - { - 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 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 index f100c91e1..3bbbbb9e1 100644 --- a/source/LibRender2/Primitives/GLDropdown.cs +++ b/source/LibRender2/Primitives/GLDropdown.cs @@ -6,89 +6,179 @@ namespace LibRender2.Primitives { - public class GLDropdown : GLControl - { - public readonly List Items = new List(); - public int SelectedIndex = 0; - public bool Expanded = false; - public EventHandler SelectionChanged; - - public GLDropdown(BaseRenderer renderer) : base(renderer) - { - IsVisible = true; - Size = new Vector2(160, 24); - BackgroundColor = new Color128(0.15f, 0.15f, 0.15f, 1f); - } - - public override void Draw() - { - if (!IsVisible) return; - Renderer.Rectangle.Draw(null, Location, Size, BackgroundColor); - - // Draw dropdown arrow - float arrowX = (float)(Location.X + Size.X - 18); - float arrowY = (float)(Location.Y + (Size.Y - 5) / 2); - 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); - } - - if (Expanded) - { - float itemHeight = 24f; - float overlayHeight = Items.Count * itemHeight; - Vector2 overlayLoc = new Vector2(Location.X, Location.Y + Size.Y); - Renderer.Rectangle.Draw(null, overlayLoc, new Vector2(Size.X, overlayHeight), new Color128(0.1f, 0.1f, 0.1f, 0.95f)); - - for (int i = 0; i < Items.Count; i++) - { - Vector2 itemLoc = 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); - } - Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Items[i], new Vector2(itemLoc.X + 8, itemLoc.Y + 2), TextAlignment.TopLeft, Color128.White); - } - } - } - - public override void MouseDown(int x, int y) - { - if (!IsVisible) return; - if (Expanded) - { - float itemHeight = 24f; - float overlayHeight = Items.Count * itemHeight; - Vector2 overlayLoc = 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; - return; - } - } - - if (x >= Location.X && x <= Location.X + Size.X && y >= Location.Y && y <= Location.Y + Size.Y) - { - Expanded = !Expanded; - } - else - { - Expanded = false; - } - } - } + 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 index 041b2b1e5..5295c650c 100644 --- a/source/LibRender2/Primitives/GLLinkButton.cs +++ b/source/LibRender2/Primitives/GLLinkButton.cs @@ -5,40 +5,60 @@ 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 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 GLLinkButton(BaseRenderer renderer) : base(renderer) + { + IsVisible = true; + Size = new Vector2(100, 20); + } - public override void Draw() - { - if (!IsVisible) return; - Color128 color = CurrentlySelected ? HoverColor : NormalColor; - 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 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 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) - { - OnClick?.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) + { + 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 index 255331cc8..10d090523 100644 --- a/source/LibRender2/Primitives/GLNumericUpDown.cs +++ b/source/LibRender2/Primitives/GLNumericUpDown.cs @@ -5,62 +5,184 @@ 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; - - 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; - Renderer.Rectangle.Draw(null, Location, new Vector2(Size.X - 34, Size.Y), BackgroundColor); - Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Value.ToString("0.##"), 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 - Renderer.Rectangle.Draw(null, upLoc, new Vector2(15, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); - 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 - Renderer.Rectangle.Draw(null, downLoc, new Vector2(15, Size.Y), new Color128(0.25f, 0.25f, 0.25f, 1f)); - 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 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) - { - 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) - { - Value = System.Math.Max(Minimum, Value - Increment); - ValueChanged?.Invoke(this, EventArgs.Empty); - OnClick?.Invoke(this, EventArgs.Empty); - } - } - } + 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/GLProgressBar.cs b/source/LibRender2/Primitives/GLProgressBar.cs index a8d0f3edf..d07d4bab7 100644 --- a/source/LibRender2/Primitives/GLProgressBar.cs +++ b/source/LibRender2/Primitives/GLProgressBar.cs @@ -3,23 +3,30 @@ namespace LibRender2.Primitives { - public class GLProgressBar : GLControl - { - public float Value = 0f; // 0 to 1 - public Color128 FillColor = Color128.Orange; + 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); - } + 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; - Renderer.Rectangle.Draw(null, Location, Size, BackgroundColor); - double fillWidth = Size.X * System.Math.Max(0.0, System.Math.Min(1.0, Value)); - Renderer.Rectangle.Draw(null, Location, new Vector2(fillWidth, Size.Y), FillColor); - } - } + 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 index 5c4f0d7be..748234c9c 100644 --- a/source/LibRender2/Primitives/GLScrollContainer.cs +++ b/source/LibRender2/Primitives/GLScrollContainer.cs @@ -1,39 +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 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; - } + 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; - Vector2 originalLocation = Children[i].Location; - Children[i].Location = new Vector2(originalLocation.X, originalLocation.Y - ScrollOffset); - if (Children[i].Location.Y >= Location.Y && Children[i].Location.Y + Children[i].Size.Y <= Location.Y + Size.Y) - { - Children[i].Draw(); - } - Children[i].Location = originalLocation; - totalHeight += Children[i].Size.Y + 4.0; - } - MaxScroll = System.Math.Max(0.0, totalHeight - Size.Y); - } + 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); - public void Scroll(double delta) - { - ScrollOffset = System.Math.Max(0.0, System.Math.Min(MaxScroll, ScrollOffset + delta)); - } - } + // 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 index d9b9b599c..8fc541b26 100644 --- a/source/LibRender2/Primitives/GLSlider.cs +++ b/source/LibRender2/Primitives/GLSlider.cs @@ -4,53 +4,66 @@ 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; + 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; + 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 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; - Renderer.Rectangle.Draw(null, new Vector2(Location.X, centerY), new Vector2(Size.X, 4.0), BackgroundColor); - - double ratio = (Value - Minimum) / (Maximum - Minimum); - double knobX = Location.X + ratio * (Size.X - 12.0); - Renderer.Rectangle.Draw(null, new Vector2(knobX, Location.Y + 2.0), new Vector2(12.0, Size.Y - 4.0), Color128.Orange); - } + 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; - if (dragging) - { - double localX = System.Math.Max(0.0, System.Math.Min(Size.X - 12.0, x - Location.X - 6.0)); - double ratio = localX / (Size.X - 12.0); - Value = Minimum + ratio * (Maximum - Minimum); - ValueChanged?.Invoke(this, EventArgs.Empty); - } - } + 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; - MouseMove(x, 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) + { + 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 index e59964f0e..2ee617533 100644 --- a/source/LibRender2/Primitives/GLTabContainer.cs +++ b/source/LibRender2/Primitives/GLTabContainer.cs @@ -6,90 +6,143 @@ 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 GLTabContainer(BaseRenderer renderer) : base(renderer) - { - IsVisible = 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); - Color128 headerBg = isSelected ? new Color128(0.2f, 0.22f, 0.25f, 0.9f) : 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); - } - - // 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 = new Vector2(Location.X, Location.Y + TabHeaderHeight); - TabContents[i].Size = new Vector2(Size.X, Size.Y - TabHeaderHeight); - TabContents[i].Draw(); - } - } - } - - 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 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/GLToggle.cs b/source/LibRender2/Primitives/GLToggle.cs index 8fc9c752e..cee3c7456 100644 --- a/source/LibRender2/Primitives/GLToggle.cs +++ b/source/LibRender2/Primitives/GLToggle.cs @@ -5,39 +5,72 @@ namespace LibRender2.Primitives { - public class GLToggle : GLControl - { - public bool Checked; - public string Text = ""; - public EventHandler ValueChanged; - - 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; - Renderer.Rectangle.Draw(null, Location, new Vector2(20, 20), BackgroundColor); - if (Checked) - { - Renderer.Rectangle.Draw(null, new Vector2(Location.X + 4, Location.Y + 4), new Vector2(12, 12), Color128.Orange); - } - Renderer.OpenGlString.Draw(Renderer.Fonts.NormalFont, Text, new Vector2(Location.X + 28, Location.Y + 2), TextAlignment.TopLeft, Color128.White); - } - - 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) - { - Checked = !Checked; - ValueChanged?.Invoke(this, EventArgs.Empty); - OnClick?.Invoke(this, EventArgs.Empty); - } - } - } + 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/ObjectViewer/Game/Menu.cs b/source/ObjectViewer/Game/Menu.cs index 9b893f832..716d3541c 100644 --- a/source/ObjectViewer/Game/Menu.cs +++ b/source/ObjectViewer/Game/Menu.cs @@ -329,6 +329,7 @@ public override void Draw(double RealTimeElapsed) if (OptionsTabContainer != null) { OptionsTabContainer.Draw(); + LibRender2.Primitives.GLDropdown.ActiveDropdown?.DrawOverlay(); } DrawSidebarToggleButton(RealTimeElapsed); return; diff --git a/source/OpenBVE/Game/Menu/Menu.cs b/source/OpenBVE/Game/Menu/Menu.cs index 76b1c0728..1091b5745 100644 --- a/source/OpenBVE/Game/Menu/Menu.cs +++ b/source/OpenBVE/Game/Menu/Menu.cs @@ -825,6 +825,7 @@ public override void Draw(double RealTimeElapsed) if (OptionsTabContainer != null) { OptionsTabContainer.Draw(); + LibRender2.Primitives.GLDropdown.ActiveDropdown?.DrawOverlay(); } DrawSidebarToggleButton(RealTimeElapsed); Renderer.PopMatrix(MatrixMode.Modelview); diff --git a/source/RouteViewer/Game/Menu.cs b/source/RouteViewer/Game/Menu.cs index 2997549e7..d911a282e 100644 --- a/source/RouteViewer/Game/Menu.cs +++ b/source/RouteViewer/Game/Menu.cs @@ -365,6 +365,7 @@ public override void Draw(double RealTimeElapsed) if (OptionsTabContainer != null) { OptionsTabContainer.Draw(); + LibRender2.Primitives.GLDropdown.ActiveDropdown?.DrawOverlay(); } DrawSidebarToggleButton(RealTimeElapsed); return; From 6e82886ee91bdd25e03b364559da972e933c5c07 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:32:08 +0700 Subject: [PATCH 7/8] Round font render coordinates to nearest integer to resolve sub-pixel blurriness --- source/LibRender2/Text/OpenGlString.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/LibRender2/Text/OpenGlString.cs b/source/LibRender2/Text/OpenGlString.cs index 85af581ca..8e81ee7f1 100644 --- a/source/LibRender2/Text/OpenGlString.cs +++ b/source/LibRender2/Text/OpenGlString.cs @@ -147,8 +147,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. @@ -209,8 +209,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. From 7ddc1e8623800d0f4e001fa5debb5851617966c0 Mon Sep 17 00:00:00 2001 From: adfriz <76892624+adfriz@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:51:12 +0700 Subject: [PATCH 8/8] mprove in-game GUI menu and label rendering - Tweak a bit of AbstractMenu for better menu layout handling - Adjust Label primitives for improved text alignment - Add menu wiring in ObjectViewer Game/Menu and GameWindow - Update RouteViewer menu, ProgramR, and GameWindow --- source/LibRender2/Menu/AbstractMenu.cs | 148 +++++++++++++++++++---- source/LibRender2/Primitives/Label.cs | 8 +- source/ObjectViewer/Game/Menu.cs | 5 + source/ObjectViewer/System/GameWindow.cs | 1 + source/RouteViewer/Game/Menu.cs | 11 +- source/RouteViewer/ProgramR.cs | 9 ++ source/RouteViewer/System/Gamewindow.cs | 1 + 7 files changed, 151 insertions(+), 32 deletions(-) diff --git a/source/LibRender2/Menu/AbstractMenu.cs b/source/LibRender2/Menu/AbstractMenu.cs index 78b3379b9..327a4d55e 100644 --- a/source/LibRender2/Menu/AbstractMenu.cs +++ b/source/LibRender2/Menu/AbstractMenu.cs @@ -349,11 +349,12 @@ public void InitializeOptionsTabContainer(HostApplication host) private GLHBoxContainer CreateRow(string name) { - var row = new GLHBoxContainer(Renderer) { Spacing = 16f }; + 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); - label.Size = new Vector2(140f, 24f); + // 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; } @@ -462,6 +463,32 @@ public Vector4 GetToggleButtonRect() 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; @@ -480,10 +507,13 @@ public void UpdateTransition(double 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) { @@ -494,6 +524,7 @@ public void UpdateTransition(double TimeElapsed) else { currentOffset = target; + SidebarFade = SidebarVisible ? 1.0 : 0.0; } menuMin.X = currentOffset; @@ -519,13 +550,26 @@ public virtual void RepositionSidebarControls() { if (child is GLHBoxContainer row) { - row.Size = new Vector2(containerWidth, row.Size.Y); + // 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]; + var label = row.Children[0] as Label; var control = row.Children[1]; - label.Size = new Vector2(containerWidth * 0.45f, row.Size.Y); - control.Size = new Vector2(containerWidth * 0.5f, row.Size.Y); + 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); } } } @@ -542,6 +586,32 @@ 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 @@ -549,29 +619,15 @@ 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) { - 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(); + ToggleSidebar(); return; } } @@ -626,10 +682,50 @@ public void ProcessMouseDown(int x, int y) } /// 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) diff --git a/source/LibRender2/Primitives/Label.cs b/source/LibRender2/Primitives/Label.cs index e8648c98a..c1193d791 100644 --- a/source/LibRender2/Primitives/Label.cs +++ b/source/LibRender2/Primitives/Label.cs @@ -25,6 +25,7 @@ using LibRender2.Text; using OpenBveApi.Colors; using OpenBveApi.Graphics; +using OpenBveApi.Math; namespace LibRender2.Primitives { @@ -40,8 +41,8 @@ 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; @@ -51,7 +52,8 @@ public Label(BaseRenderer renderer, string text) : base(renderer) 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/ObjectViewer/Game/Menu.cs b/source/ObjectViewer/Game/Menu.cs index 716d3541c..5a0cf5b0b 100644 --- a/source/ObjectViewer/Game/Menu.cs +++ b/source/ObjectViewer/Game/Menu.cs @@ -242,6 +242,10 @@ 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) { @@ -450,6 +454,7 @@ public override void Draw(double RealTimeElapsed) menu.Align, ColourDimmed, false); DrawSidebarToggleButton(RealTimeElapsed); + DrawSidebarResizeGrip(); } } } 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/RouteViewer/Game/Menu.cs b/source/RouteViewer/Game/Menu.cs index d911a282e..9d5b6607d 100644 --- a/source/RouteViewer/Game/Menu.cs +++ b/source/RouteViewer/Game/Menu.cs @@ -277,6 +277,10 @@ 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) { @@ -356,12 +360,12 @@ public override void Draw(double RealTimeElapsed) } 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), backgroundColor); + 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(); @@ -505,6 +509,7 @@ public override void Draw(double RealTimeElapsed) } DrawSidebarToggleButton(RealTimeElapsed); + DrawSidebarResizeGrip(); } private static void routeWorkerThread_doWork(object sender, DoWorkEventArgs e) diff --git a/source/RouteViewer/ProgramR.cs b/source/RouteViewer/ProgramR.cs index 95c2cb2f0..b553e5512 100644 --- a/source/RouteViewer/ProgramR.cs +++ b/source/RouteViewer/ProgramR.cs @@ -436,6 +436,15 @@ 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); 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)