Example #1
0
            public void SetupExtraButtons(MainMenuWindow window,
                                          UBuilder builder,
                                          AddButtonsResult toolButtonsResult)
            {
                this.name = "TMPE_MainMenu_ExtraPanel";

                // Silver background panel
                this.atlas            = TextureUtil.Ingame;
                this.backgroundSprite = "GenericPanel";

                // The panel will be Dark Silver at 50% dark 100% alpha
                this.color = new Color32(128, 128, 128, 255);

                this.ResizeFunction(
                    resizeFn: (UResizer r) => {
                    // Step to the right by 4px
                    r.Stack(
                        mode: UStackMode.ToTheRight,
                        spacing: UConst.UIPADDING);
                    r.FitToChildren();
                });

                // Place two extra buttons (despawn & clear traffic).
                // Use as many rows as in the other panel.
                var extraButtonsResult = AddButtonsFromButtonDefinitions(
                    window,
                    parentComponent: this,
                    builder,
                    buttonDefs: EXTRA_BUTTON_DEFS,
                    minRowLength: toolButtonsResult.Layout.Rows == 2 ? 1 : 2);

                window.ExtraButtonsList = extraButtonsResult.Buttons;
            }
        /// <summary>
        /// On Screen Display feature:
        /// Clear, and hide the keybind panel.
        /// Populate with items, which can be keyboard shortcuts or hardcoded mouse clicks.
        /// </summary>
        /// <param name="items">List of <see cref="OsdItem"/> to display.</param>
        public static void Display(List <OsdItem> items)
        {
            MainMenuWindow mainMenu = ModUI.Instance.MainMenu;

            Hide();

            // Deactivate old items, and destroy them. Also remove them from the panel till Unity
            // is happy to delete them.
            foreach (Transform c in mainMenu.OnscreenDisplayPanel.transform)
            {
                c.gameObject.SetActive(false);
                UnityEngine.Object.Destroy(c.gameObject);
            }

            // mainMenu.KeybindsPanel.transform.DetachChildren();

            // Populate the panel with the items
            var builder = new UBuilder();

            foreach (OsdItem item in items)
            {
                item.Build(
                    parent: mainMenu.OnscreenDisplayPanel,
                    builder);
            }

            if (items.Count > 0 &&
                mainMenu.OnscreenDisplayPanel.GetUIView() != null)
            {
                mainMenu.OnscreenDisplayPanel.opacity = 1f; // fully visible, opaque
            }

            // Recalculate now
            UResizer.UpdateControl(mainMenu);
        }
Example #3
0
            /// <summary>Create buttons and add them to the given panel UIBuilder.</summary>
            /// <param name="window">The parent window.</param>
            /// <param name="parentComponent">The parent panel component to host the buttons.</param>
            /// <param name="builder">UI builder to use.</param>
            /// <param name="buttonDefs">The button definitions array.</param>
            /// <param name="minRowLength">Shortest horizontal row length allowed before breaking new row.</param>
            /// <returns>A list of created buttons.</returns>
            private AddButtonsResult AddButtonsFromButtonDefinitions(
                MainMenuWindow window,
                UIComponent parentComponent,
                UBuilder builder,
                MenuButtonDef[] buttonDefs,
                int minRowLength)
            {
                AddButtonsResult result;

                result.Buttons = new List <BaseMenuButton>();

                // Count the button objects and set their layout
                result.Layout = new MainMenuLayout();
                result.Layout.CountEnabledButtons(buttonDefs);
                int placedInARow = 0;

                foreach (MenuButtonDef buttonDef in buttonDefs)
                {
                    if (!buttonDef.IsEnabledFunc())
                    {
                        // Skip buttons which are not enabled
                        continue;
                    }

                    // Create and populate the panel with buttons
                    var button = parentComponent.AddUIComponent(buttonDef.ButtonType) as BaseMenuButton;

                    // Count buttons in a row and break the line
                    bool doRowBreak = result.Layout.IsRowBreak(placedInARow, minRowLength);

                    button.ResizeFunction(
                        resizeFn: (UResizer r) => {
                        r.Stack(doRowBreak ? UStackMode.NewRowBelow : UStackMode.ToTheRight);
                        r.Width(UValue.FixedSize(40f));
                        r.Height(UValue.FixedSize(40f));
                    });

                    if (doRowBreak)
                    {
                        placedInARow = 0;
                        result.Layout.Rows++;
                    }
                    else
                    {
                        placedInARow++;
                    }

                    // Also ask each button what sprites they need
                    button.SetupButtonSkin(builder.AtlasBuilder);

                    // Take button classname, split by ".", and the last word becomes the button name
                    string buttonName = buttonDef.ButtonType.ToString().Split('.').Last();
                    button.name = $"TMPE_MainMenuButton_{buttonName}";

                    window.ButtonsDict.Add(buttonDef.Mode, button);
                    result.Buttons.Add(button);
                }

                return(result);
            }
            /// <summary>
            /// Sets up two stacked labels: for mode description (what the user sees) and for hint.
            /// The text is empty but is updated after every mode or view change.
            /// </summary>
            public void SetupControls(SpeedLimitsToolWindow window,
                                      UBuilder builder,
                                      SpeedLimitsTool parentTool)
            {
                this.position = Vector3.zero;

                this.backgroundSprite = "GenericPanel";
                this.color            = new Color32(64, 64, 64, 255);

                this.SetPadding(UPadding.Default);
                this.ResizeFunction(
                    resizeFn: (UResizer r) => {
                    r.Stack(
                        UStackMode.ToTheRight,
                        spacing: UConst.UIPADDING,
                        stackRef: window.modeButtonsPanel_);
                    r.FitToChildren();
                });

                this.ModeDescriptionLabel = builder.Label_(
                    parent: this,
                    t: string.Empty,
                    stack: UStackMode.Below,
                    processMarkup: true);
                this.ModeDescriptionLabel.SetPadding(
                    new UPadding(top: 12f, right: 0f, bottom: 0f, left: 0f));
            }
        /// <summary>Creates a draggable label with current unit (mph or km/h).</summary>
        /// <param name="builder">The UI builder to use.</param>
        private void SetupControls_TitleBar(UBuilder builder)
        {
            string unitTitle = string.Format(
                format: "{0} - {1}",
                Translation.SpeedLimits.Get("Window.Title:Speed Limits"),
                GlobalConfig.Instance.Main.DisplaySpeedLimitsMph
                    ? Translation.SpeedLimits.Get("Miles per hour")
                    : Translation.SpeedLimits.Get("Kilometers per hour"));

            // The label will be repositioned to the top of the parent
            this.windowTitleLabel_ = builder.Label_(
                parent: this,
                t: unitTitle,
                stack: UStackMode.Below);

            this.dragHandle_ = this.CreateDragHandle();

            // On window drag - clamp to screen and then save in the config
            this.eventPositionChanged += (_, value) => {
                GlobalConfig.Instance.Main.SpeedLimitsWindowX = (int)value.x;
                GlobalConfig.Instance.Main.SpeedLimitsWindowY = (int)value.y;

                if (!queuedClampToScreen)
                {
                    // Prevent multiple invocations by setting a flag
                    queuedClampToScreen = true;
                    Invoke(eventName: "OnPeriodicClampToScreen", 2.0f);
                }
            };
        }
Example #6
0
            public void SetupControls(MainMenuWindow window, UBuilder builder)
            {
                this.name = "TMPE_MainMenu_KeybindsPanel";

                // the GenericPanel sprite is Light Silver, make it dark
                this.atlas            = TextureUtil.Ingame;
                this.backgroundSprite = "GenericPanel";
                this.color            = new Color32(64, 64, 64, 240);
                this.opacity          = GlobalConfig.Instance.Main.KeybindsPanelVisible
                                   ? 1f
                                   : 0f;

                this.SetPadding(UPadding.Default);

                // The keybinds panel belongs to main menu but does not expand it to fit
                this.ContributeToBoundingBox(false);

                this.ResizeFunction(
                    resizeFn: (UResizer r) => {
                    r.Stack(mode: UStackMode.NewRowBelow, spacing: UConst.UIPADDING * 2);

                    // As the control technically belongs inside the mainmenu, it will respect
                    // the 4px padding, we want to shift it slightly left to line up with the
                    // main menu panel.
                    r.MoveBy(new Vector2(-UConst.UIPADDING, 0f));
                    r.FitToChildren();
                });
            }
Example #7
0
            /// <summary>Create speeds palette based on the current options choices.</summary>
            /// <param name="window">Containing <see cref="SpeedLimitsToolWindow"/>.</param>
            /// <param name="builder">The UI builder to use.</param>
            /// <param name="parentTool">The tool object.</param>
            public void SetupControls(SpeedLimitsToolWindow window, UBuilder builder, SpeedLimitsTool parentTool)
            {
                this.parentTool_ = parentTool;
                this.name        = GAMEOBJECT_NAME + "_PalettePanel";
                this.position    = Vector3.zero;
                this.SetPadding(UPadding.Default);

                this.ResizeFunction(
                    resizeFn: r => {
                    r.Stack(
                        mode: UStackMode.Below,
                        stackRef: window.modeDescriptionWrapPanel_);
                    r.FitToChildren();
                });
                bool showMph = GlobalConfig.Instance.Main.DisplaySpeedLimitsMph;

                // Fill with buttons
                // [ 10 20 30 ... 120 130 140 0(no limit) ]
                //-----------------------------------------
                // the Current Selected Speed is highlighted
                List <SetSpeedLimitAction> actions = new();

                actions.Add(SetSpeedLimitAction.ResetToDefault()); // add: Default
                actions.AddRange(PaletteGenerator.AllSpeedLimits(SpeedUnit.CurrentlyConfigured));
                actions.Add(SetSpeedLimitAction.Unlimited());      // add: Unlimited

                this.buttonsByNumber_ = new();
                this.PaletteButtons.Clear();

                foreach (SetSpeedLimitAction action in actions)
                {
                    SpeedLimitPaletteButton nextButton = this.SetupControls_SpeedPalette_Button(
                        builder: builder,
                        parent: this,
                        parentTool: parentTool,
                        showMph: showMph,
                        actionOnClick: action);
                    this.PaletteButtons.Add(nextButton);

                    // If this is a numbered button, and its a multiple of 10...
                    if (action.Type == SetSpeedLimitAction.ActionType.SetOverride)
                    {
                        int number = (int)(showMph
                                               ? action.GuardedValue.Override.GetMph()
                                               : action.GuardedValue.Override.GetKmph());
                        this.buttonsByNumber_.Add(number, nextButton);
                    }
                    else if (action.Type == SetSpeedLimitAction.ActionType.Unlimited)
                    {
                        this.unlimitedButton_ = nextButton;
                    }
                    else if (action.Type == SetSpeedLimitAction.ActionType.ResetToDefault)
                    {
                        this.resetToDefaultButton_ = nextButton;
                    }
                }
            }
Example #8
0
            /// <summary>
            /// Creates a button with speed value on it, and label under it, showing opposite units.
            /// Also can be zero (reset to default) and 1000 km/h (unlimited speed button).
            /// </summary>
            /// <param name="builder">UI builder.</param>
            /// <param name="actionOnClick">What happens if clicked.</param>
            /// <param name="parentTool">Parent speedlimits tool.</param>
            /// <param name="buttonPanel">Panel where buttons are added to.</param>
            /// <param name="speedInteger">Integer value of the speed in the selected units.</param>
            /// <param name="speedValue">Speed value of the button we're creating.</param>
            /// <returns>The new button.</returns>
            private SpeedLimitPaletteButton CreatePaletteButton(UBuilder builder,
                                                                SetSpeedLimitAction actionOnClick,
                                                                SpeedLimitsTool parentTool,
                                                                UPanel buttonPanel,
                                                                int speedInteger,
                                                                SpeedValue speedValue)
            {
                // Helper function to choose text for the button
                string GetSpeedButtonText()
                {
                    if (speedInteger == 0)
                    {
                        return("✖"); // Unicode symbol U+2716 Heavy Multiplication X
                    }

                    if (speedValue.GameUnits >= SpeedValue.UNLIMITED)
                    {
                        return("⊘"); // Unicode symbol U+2298 Circled Division Slash
                    }

                    return(speedInteger.ToString());
                }

                var button = builder.Button <SpeedLimitPaletteButton>(parent: buttonPanel);

                button.text      = GetSpeedButtonText();
                button.textScale = UIScaler.UIScale;
                button.textHorizontalAlignment = UIHorizontalAlignment.Center;

                button.normalBgSprite = button.hoveredBgSprite = "GenericPanel";
                button.color          = new Color32(128, 128, 128, 240);

                // button must know what to do with its speed value
                button.AssignedAction = actionOnClick;

                // The click events will be routed via the parent tool OnPaletteButtonClicked
                button.ParentTool = parentTool;

                button.SetStacking(UStackMode.NewRowBelow);

                // Width will be overwritten in SpeedLimitPaletteButton.UpdateSpeedLimitButton
                button.SetFixedSize(
                    new Vector2(
                        SpeedLimitPaletteButton.DEFAULT_WIDTH,
                        SpeedLimitPaletteButton.DEFAULT_HEIGHT));
                return(button);
            }
        /// <summary>Populate the window using UIBuilder of the window panel.</summary>
        /// <param name="builder">The root builder of this window.</param>
        public void SetupControls(UBuilder builder, SpeedLimitsTool parentTool)
        {
            this.parentTool_ = parentTool;

            // "Speed Limits - Kilometers per Hour"
            // "Showing speed limit overrides per road segment."
            // [ Lane/Segment ] [ 10 20 30 40 50 ... 120 130 140 Max Reset]
            // [ Edit Default ] |   |  |  |  |  |   |   |   |   |   |     |
            // [_MPH/KM_______] [___+__+__+__+__+...+___+___+___+___+_____]

            // Goes first on top of the window
            SetupControls_TitleBar(builder);

            // Vertical panel goes under the titlebar
            SetupControls_ModeButtons(builder);

            // Text below for "Current mode: " and "Hold Alt, hold Shift, etc..."
            modeDescriptionWrapPanel_ =
                builder.Panel <ModeDescriptionPanel>(
                    parent: this,
                    stack: UStackMode.None);
            modeDescriptionWrapPanel_.SetupControls(window: this, builder, parentTool);

            // Palette: Goes right of the modebuttons panel
            palettePanel_ = builder.Panel <PalettePanel>(
                parent: this,
                stack: UStackMode.None);
            palettePanel_.SetupControls(window: this, builder, parentTool);

            // palette was built for the current configured MPH/KM display
            this.DisplaySpeedLimitsMph = GlobalConfig.Instance.Main.DisplaySpeedLimitsMph;

            cursorTooltip_ = builder.Label <UFloatingTooltip>(
                parent: this,
                t: string.Empty,
                stack: UStackMode.None);

            // this will hide it, and update it after setup is done
            cursorTooltip_.SetTooltip(t: null, show: false);

            this.gameObject.AddComponent <CustomKeyHandler>();

            // Force buttons resize and show the current speed limit on the palette
            this.UpdatePaletteButtonsOnClick();
            this.FocusWindow();
        }
Example #10
0
            SetupControls_SpeedPalette_Button(UBuilder builder,
                                              UIComponent parent,
                                              bool showMph,
                                              SetSpeedLimitAction actionOnClick,
                                              SpeedLimitsTool parentTool)
            {
                SpeedValue speedValue =
                    actionOnClick.Type == SetSpeedLimitAction.ActionType.ResetToDefault
                        ? default
                        : actionOnClick.GuardedValue.Override;

                int speedInteger = showMph
                                       ? speedValue.ToMphRounded(RoadSignThemes.MPH_STEP).Mph
                                       : speedValue.ToKmphRounded(RoadSignThemes.KMPH_STEP).Kmph;

                //--------------------------------
                // Create vertical combo:
                // |[  100   ]|
                // | "65 mph" |
                //--------------------------------
                // Create a small panel which stacks together with other button panels horizontally
                var buttonPanel = builder.Panel_(parent: parent);

                buttonPanel.name = $"{GAMEOBJECT_NAME}_Button_{speedInteger}";
                buttonPanel.ResizeFunction(
                    resizeFn: (UResizer r) => {
                    r.Stack(UStackMode.ToTheRight, spacing: 2f);
                    r.FitToChildren();
                });

                SpeedLimitPaletteButton button = this.CreatePaletteButton(
                    builder,
                    actionOnClick,
                    parentTool,
                    buttonPanel,
                    speedInteger,
                    speedValue);

                this.CreatePaletteButtonHintLabel(builder, showMph, speedValue, button, buttonPanel);
                return(button);
            }
Example #11
0
            public AddButtonsResult SetupToolButtons(MainMenuWindow window, UBuilder builder)
            {
                this.name = "TMPE_MainMenu_ToolPanel";
                this.ResizeFunction(
                    (UResizer r) => {
                    r.Stack(mode: UStackMode.Below);
                    r.FitToChildren();
                });

                // Create 1 or 2 rows of button objects
                var toolButtonsResult = AddButtonsFromButtonDefinitions(
                    window,
                    parentComponent: this,
                    builder,
                    buttonDefs: TOOL_BUTTON_DEFS,
                    minRowLength: 4);

                window.ToolButtonsList = toolButtonsResult.Buttons;

                return(toolButtonsResult);
            }
Example #12
0
            private void CreatePaletteButtonHintLabel(UBuilder builder,
                                                      bool showMph,
                                                      SpeedValue speedValue,
                                                      SpeedLimitPaletteButton button,
                                                      UPanel buttonPanel)
            {
                // Other speed unit info label
                string otherUnit = showMph
                                       ? ToKmphPreciseString(speedValue)
                                       : ToMphPreciseString(speedValue);

                // Choose label text under the button
                string GetSpeedButtonHintText()
                {
                    if (FloatUtil.NearlyEqual(speedValue.GameUnits, 0.0f))
                    {
                        return(Translation.SpeedLimits.Get("Palette.Text:Default"));
                    }

                    if (speedValue.GameUnits >= SpeedValue.UNLIMITED)
                    {
                        return(Translation.SpeedLimits.Get("Palette.Text:Unlimited"));
                    }

                    return(otherUnit);
                }

                ULabel label = button.AltUnitsLabel =
                    builder.Label_(
                        parent: buttonPanel,
                        t: GetSpeedButtonHintText(),
                        stack: UStackMode.Below);

                label.width         = SpeedLimitPaletteButton.SELECTED_WIDTH;
                label.textAlignment = UIHorizontalAlignment.Center;
                label.ContributeToBoundingBox(false); // parent ignore our width
            }
 /// <summary>Create mode buttons panel on the left side.</summary>
 /// <param name="builder">The UI builder to use.</param>
 private void SetupControls_ModeButtons(UBuilder builder)
 {
     modeButtonsPanel_ = builder.Panel <ModeButtonsPanel>(parent: this);
     modeButtonsPanel_.SetupControls(window: this, builder);
 }
Example #14
0
            public void SetupControls(SpeedLimitsToolWindow window, UBuilder builder)
            {
                this.name = GAMEOBJECT_NAME + "_ModesPanel";

                void ButtonpanelResizeFn(UResizer r)
                {
                    r.Stack(
                        mode: UStackMode.NewRowBelow,
                        spacing: UConst.UIPADDING,
                        stackRef: window.windowTitleLabel_);
                    r.FitToChildren();
                }

                this.ResizeFunction(ButtonpanelResizeFn);

                Vector2        buttonSize  = new Vector2(40f, 40f);
                UITextureAtlas uiAtlas     = window.GetUiAtlas();
                LookupTable    translation = Translation.SpeedLimits;

                //----------------
                // Edit Segments/Lanes mode button
                //----------------
                this.SegmentModeButton = builder.Button <UButton>(
                    parent: this,
                    text: string.Empty,
                    tooltip: translation.Get("Tooltip:Edit segment speed limits"),
                    size: buttonSize,
                    stack: UStackMode.Below);
                this.SegmentModeButton.atlas = uiAtlas;

                // Note the atlas is loaded before this skin is created in window.GetUiAtlas()
                this.SegmentModeButton.Skin =
                    ButtonSkin.CreateSimple(
                        foregroundPrefix: "EditSegments",
                        backgroundPrefix: UConst.MAINMENU_ROUND_BUTTON_BG)
                    .CanActivate(background: false)
                    .CanHover(foreground: false);
                this.SegmentModeButton.ApplyButtonSkin();

                // the onclick handler is set by SpeedLimitsTool outside of this module

                //----------------
                // Edit Lanes mode button
                //----------------
                this.LaneModeButton = builder.Button <UButton>(
                    parent: this,
                    text: string.Empty,
                    tooltip: translation.Get("Tooltip:Edit lane speed limits"),
                    size: buttonSize,
                    stack: UStackMode.ToTheRight);
                this.LaneModeButton.atlas = uiAtlas;
                // Note the atlas is loaded before this skin is created in window.GetUiAtlas()
                this.LaneModeButton.Skin = ButtonSkin
                                           .CreateSimple(
                    foregroundPrefix: "EditLanes",
                    backgroundPrefix: UConst.MAINMENU_ROUND_BUTTON_BG)
                                           .CanActivate(background: false)
                                           .CanHover(foreground: false);
                this.LaneModeButton.ApplyButtonSkin();
                // the onclick handler is set by SpeedLimitsTool outside of this module

                //----------------
                // Edit Defaults mode button
                //----------------
                this.DefaultsModeButton = builder.Button <UButton>(
                    parent: this,
                    text: string.Empty,
                    tooltip: translation.Get("Tooltip:Default speed limits per road type"),
                    size: buttonSize,
                    stack: UStackMode.NewRowBelow);
                this.DefaultsModeButton.atlas = uiAtlas;

                // Note the atlas is loaded before this skin is created in window.GetUiAtlas()
                this.DefaultsModeButton.Skin = ButtonSkin
                                               .CreateSimple(
                    foregroundPrefix: "EditDefaults",
                    backgroundPrefix: UConst.MAINMENU_ROUND_BUTTON_BG)
                                               .CanActivate(background: false)
                                               .CanHover(foreground: false);
                this.DefaultsModeButton.ApplyButtonSkin();

                // the onclick handler is set by SpeedLimitsTool outside of this module

                //----------------
                // MPH/Kmph switch
                //----------------
                bool displayMph = GlobalConfig.Instance.Main.DisplaySpeedLimitsMph;

                this.ToggleMphButton = builder.Button <MphToggleButton>(
                    parent: this,
                    text: string.Empty,
                    tooltip: displayMph
                                 ? translation.Get("Miles per hour")
                                 : translation.Get("Kilometers per hour"),
                    size: buttonSize,
                    stack: UStackMode.ToTheRight);
                this.ToggleMphButton.atlas = uiAtlas;

                // Note the atlas is loaded before this skin is created in window.GetUiAtlas()
                this.ToggleMphButton.Skin = ButtonSkin.CreateSimple(
                    foregroundPrefix: "MphToggle",
                    backgroundPrefix: UConst.MAINMENU_ROUND_BUTTON_BG)
                                            .CanActivate(background: false)
                                            .CanHover(foreground: false);
                this.ToggleMphButton.ApplyButtonSkin();

                // the onclick handler is set by SpeedLimitsTool outside of this module
            }
        /// <summary>
        /// Create button triples for number of lanes.
        /// Buttons are linked to lanes later by LaneArrowTool class.
        /// </summary>
        /// <param name="builder">The UI Builder.</param>
        /// <param name="numLanes">How many lane groups.</param>
        public void SetupControls(UBuilder builder, int numLanes)
        {
            Buttons = new List <LaneArrowButton>();

            var buttonRowPanel = builder.Panel_(parent: this, stack: UStackMode.NewRowBelow);

            buttonRowPanel.name = "TMPE_ButtonRow";
            buttonRowPanel.SetPadding(UPadding.Default);
            buttonRowPanel.ResizeFunction((UResizer r) => { r.FitToChildren(); });

            // -----------------------------------
            // Create a row of button groups
            //      [ Lane 1      ] [ Lane 2 ] [ Lane 3 ] ...
            //      [ [←] [↑] [→] ] [...     ] [ ...    ]
            // -----------------------------------
            for (var i = 0; i < numLanes; i++)
            {
                string buttonName       = $"TMPE_LaneArrow_ButtonGroup{i + 1}";
                UPanel buttonGroupPanel = builder.Panel_(
                    parent: buttonRowPanel,
                    stack: i == 0 ? UStackMode.Below : UStackMode.ToTheRight);
                buttonGroupPanel.name             = buttonName;
                buttonGroupPanel.atlas            = TextureUtil.Ingame;
                buttonGroupPanel.backgroundSprite = "GenericPanel";

                int i1 = i; // copy of the loop variable, for the resizeFunction below

                buttonGroupPanel.ResizeFunction((UResizer r) => { r.FitToChildren(); });
                buttonGroupPanel.SetPadding(UPadding.Default);

                // Create a label with "Lane #" title
                string labelText = Translation.LaneRouting.Get("Format.Label:Lane") + " " +
                                   (i + 1);
                ULabel laneLabel = builder.Label_(
                    parent: buttonGroupPanel,
                    t: labelText);

                // The label will be repositioned to the top of the parent
                laneLabel.ResizeFunction(r => { r.Stack(UStackMode.Below); });

                // Create and populate the panel with buttons
                // 3 buttons are created [←] [↑] [→],
                // The click event is assigned outside in LaneArrowTool.cs
                foreach (string prefix in new[] {
                    "LaneArrowLeft",
                    "LaneArrowForward",
                    "LaneArrowRight",
                })
                {
                    LaneArrowButton arrowButton = builder.Button <LaneArrowButton>(
                        parent: buttonGroupPanel,
                        text: string.Empty,
                        tooltip: null,
                        size: new Vector2(40f, 40f),
                        stack: prefix == "LaneArrowLeft"
                                   ? UStackMode.Below
                                   : UStackMode.ToTheRight);
                    arrowButton.atlas = GetAtlas();
                    arrowButton.Skin  = CreateDefaultButtonSkin();
                    arrowButton.Skin.ForegroundPrefix = prefix;
                    Buttons.Add(arrowButton);
                } // for each button
            }     // end button loop, for each lane
        }