Skip to content

Instantly share code, notes, and snippets.

@Stuyk
Last active April 30, 2019 19:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Stuyk/9710f8808f62195e9b6c8b26f203571d to your computer and use it in GitHub Desktop.
Save Stuyk/9710f8808f62195e9b6c8b26f203571d to your computer and use it in GitHub Desktop.
Not complete that's for sure.
/* Created by STUYK.
* Easily allows the creation of Menus in a grid space.
* With various components including InputBoxes and Draggables.
* Components are easy to write and are inherited easily.
*/
using RAGE;
using RageFont = RAGE.Game.Font;
using Ui = RAGE.Game.Ui;
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using CoffeeVClient.Utility;
using System.Text.RegularExpressions;
using System.Linq;
using System.Reflection;
namespace CoffeeVClient.Sandbox
{
public class MenuBuilder : Events.Script
{
public static Menu CurrentMenu = null;
public MenuBuilder()
{
Events.Tick += Tick;
Events.Add("InputKeyPress", InputKeyPress);
Events.Add("InputKeyBack", InputKeyBack);
Events.Add("InputKeyEnter", InputKeyEnter);
Events.Add("InputKeyTab", InputKeyTab);
}
public static void ClearMenu()
{
CurrentMenu = null;
Ui.DisplayHud(true);
Ui.DisplayRadar(true);
Chat.Show(true);
RAGE.Game.Graphics.TransitionFromBlurred(2500);
}
private void InputKeyTab(object[] args)
{
if (CurrentMenu == null)
return;
if (CurrentMenu.Inputs.Count <= 0)
return;
int index = 0;
InputBox[] inputBoxes = CurrentMenu.Inputs.FindAll(x => x.Tabbable).ToArray();
if (inputBoxes.Length <= 0)
return;
if (CurrentMenu.SelectedDrawable != null)
{
CurrentMenu.SelectedDrawable.Selected = false;
for (int i = 0; i < inputBoxes.Length; i++)
{
if (inputBoxes[i] != CurrentMenu.SelectedDrawable)
continue;
index = i + 1;
break;
}
}
if (index > inputBoxes.Length - 1)
index = 0;
CurrentMenu.SelectedDrawable = inputBoxes[index];
inputBoxes[index].Selected = true;
}
private void InputKeyEnter(object[] args)
{
if (CurrentMenu == null)
return;
Button[] result = CurrentMenu.Buttons.FindAll(x => x.Enterable).ToArray();
if (result == null || result.Length <= 0)
return;
for(int i = 0; i < result.Length; i++)
{
if (result[i].Function == null)
continue;
result[i].Function.Invoke();
}
}
private void InputKeyBack(object[] args)
{
if (CurrentMenu == null)
return;
if (CurrentMenu.SelectedDrawable == null)
return;
string str = CurrentMenu.SelectedDrawable.Text;
if (str == "")
return;
CurrentMenu.SelectedDrawable.Text = str.Substring(0, (str.Length - 1));
}
private void InputKeyPress(object[] args)
{
if (args[0] == null)
return;
if (CurrentMenu == null)
return;
if (CurrentMenu.SelectedDrawable == null)
return;
CurrentMenu.SelectedDrawable.Text += args[0].ToString();
}
private void Tick(List<Events.TickNametagData> nametags)
{
if (CurrentMenu == null)
return;
if (!CurrentMenu.IsReady)
return;
RAGE.Game.Pad.DisableAllControlActions(0);
Ui.ShowCursorThisFrame();
CurrentMenu.Draw();
}
}
public class Menu
{
public List<Button> Buttons = new List<Button>();
public List<InputBox> Inputs = new List<InputBox>();
public List<Header> Headers = new List<Header>();
public List<TextBox> TextBoxes = new List<TextBox>();
public List<Draggable> Draggables = new List<Draggable>();
public Notification Notification = null;
public dynamic SelectedDrawable = null;
public bool IsReady = false;
public bool BlurOnStart = false;
public Vector2 CustomGrid = new Vector2()
{
X = 16,
Y = 16
};
/// <summary>
/// Create a new Menu to add components to.
/// </summary>
public Menu() { }
public void Draw()
{
for (int i = 0; i < Buttons.Count; i++)
Buttons[i].Draw();
for (int i = 0; i < Inputs.Count; i++)
Inputs[i].Draw();
for (int i = 0; i < Headers.Count; i++)
Headers[i].Draw();
for (int i = 0; i < TextBoxes.Count; i++)
TextBoxes[i].Draw();
for (int i = 0; i < Draggables.Count; i++)
Draggables[i].Draw();
if (Notification != null)
Notification.Draw();
}
public void SetColorTemplateForAll(ColorTemplate colorTemplate)
{
for (int i = 0; i < Buttons.Count; i++)
Buttons[i].ColorTemplate = colorTemplate;
for (int i = 0; i < Inputs.Count; i++)
Inputs[i].ColorTemplate = colorTemplate;
for (int i = 0; i < Headers.Count; i++)
Headers[i].ColorTemplate = colorTemplate;
for (int i = 0; i < TextBoxes.Count; i++)
TextBoxes[i].ColorTemplate = colorTemplate;
for (int i = 0; i < Draggables.Count; i++)
Draggables[i].ColorTemplate = colorTemplate;
}
public void DisplayNotification(Notification notification)
{
Notification = notification;
Notification.Alpha = 255;
}
public void SetMenuAsReady()
{
MenuBuilder.CurrentMenu = this;
IsReady = true;
RealScreenRes.GridWidthCount = CustomGrid.X;
RealScreenRes.GridHeightCount = CustomGrid.Y;
Ui.DisplayHud(false);
Ui.DisplayRadar(false);
Chat.Show(false);
if (BlurOnStart)
RAGE.Game.Graphics.TransitionToBlurred(500);
}
public void SetHeaderTextByTarget(string name, string value)
{
var result = Headers.Find(x => x.TargetName == name);
if (result == null)
return;
result.Text = value;
}
}
public class Header : Drawable
{
public string TargetName = "";
public override void Draw()
{
DrawInheritance();
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.Color);
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, false);
}
/// <summary>
/// Displays just plain text and mostly used as a spacer.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="w"></param>
/// <param name="h"></param>
/// <param name="text"></param>
public Header(int x, int y, int w, int h, string text)
{
X = x;
Y = y;
W = w;
H = h;
Text = text;
}
}
public class Button : Drawable
{
public override void Draw()
{
DrawInheritance();
if (Hovered)
{
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.ColorHover);
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextHoverColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextHoverColor, Font, true);
return;
}
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.Color);
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
}
/// <summary>
/// This uses a grid system. 0 - 16 for X and Y.
/// Width also uses the grid system. If you specify 0 it will take up 2 grid width.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="w"></param>
/// <param name="h"></param>
/// <param name="text"></param>
public Button(int x, int y, int w, int h, string text)
{
X = x;
Y = y;
W = w;
H = h;
Text = text;
Hoverable = true;
Clickable = true;
}
}
public class Notification : Drawable
{
public int Alpha = 255;
public int Tick = 0;
public override void Draw()
{
if (Alpha <= 0)
return;
DrawInheritance();
Tick++;
if (Tick % 5 == 0)
Alpha -= 2;
if (Tick > 100)
Tick = 0;
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.Color);
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
}
/// <summary>
/// A notification will display for a short period of time and then disappear.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="w"></param>
/// <param name="h"></param>
/// <param name="text"></param>
public Notification(int x, int y, int w, int h, string text)
{
X = x;
Y = y;
W = w;
H = h;
Text = text;
}
}
public class InputBox : Drawable
{
public override void Draw()
{
DrawInheritance();
if (Hovered)
{
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.ColorHover);
if (TextPrivate)
{
string newText = new string('*', Text.Length);
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(newText, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextHoverColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(newText, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextHoverColor, Font, true);
}
else
{
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextHoverColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextHoverColor, Font, true);
}
if (Selected)
DrawSelected();
if (!Selected)
DrawUnSelected();
return;
}
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.Color);
if (TextPrivate)
{
string newText = new string('*', Text.Length);
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(newText, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(newText, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
}
else
{
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + TextLeftPadding, Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
}
if (!Selected)
{
DrawUnSelected();
return;
}
DrawSelected();
}
private void DrawSelected()
{
int fixedY = (Calc_H / 3);
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y + (fixedY / 2)), new Size(Calc_W, Calc_H - fixedY), ColorTemplate.SelectedColor);
}
private void DrawUnSelected()
{
int fixedY = (Calc_H / 3);
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y + (fixedY / 2)), new Size(Calc_W, Calc_H - fixedY), ColorTemplate.UnSelectedColor);
}
/// <summary>
/// Creates a selectable input box that can be typed or tabbed into.
/// This uses a grid system. 0 - 16 for X and Y.
/// Width also uses the grid system. If you specify 0 it will take up 2 grid width.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="w"></param>
/// <param name="h"></param>
/// <param name="text"></param>
public InputBox(int x, int y, int w, int h)
{
X = x;
Y = y;
W = w;
H = h;
Hoverable = true;
Clickable = true;
Selectable = true;
Typeable = true;
Tabbable = true;
}
}
public class TextBox : Drawable
{
public override void Draw()
{
DrawInheritance();
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.Color);
for(int i = 0; i < TextScrollableList.Count; i++)
{
if (TextAlignment == Alignment.Center)
RAGE.Game.UIText.Draw(TextScrollableList[i], new Point(Calc_X + (Calc_W / 2) + TextLeftPadding, Calc_Y + 5), TextScale, ColorTemplate.TextColor, Font, true);
if (TextAlignment == Alignment.Left)
RAGE.Game.UIText.Draw(TextScrollableList[i], new Point(Calc_X + TextLeftPadding, Calc_Y + 5), TextScale, ColorTemplate.TextColor, Font, false);
}
}
/// <summary>
/// Displays a scrollable section of text.
/// Don't worry about adding new lines. Just put all your text inside of the text variable.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="w"></param>
/// <param name="h"></param>
/// <param name="text"></param>
public TextBox(int x, int y, int w, int h, string text)
{
X = x;
Y = y;
W = w;
H = h;
Text = text;
Scrollable = true;
Hoverable = true;
}
}
public class Draggable : Drawable
{
public override void Draw()
{
DrawInheritance();
if (Dragging)
{
RAGE.Game.UIRectangle.Draw(new Point(Drag_Calc_X, Drag_Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.Color);
RAGE.Game.UIText.Draw(Text, new Point(Drag_Calc_X + (Calc_W / 2), Drag_Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
return;
}
RAGE.Game.UIRectangle.Draw(new Point(Calc_X, Calc_Y), new Size(Calc_W, Calc_H), ColorTemplate.Color);
RAGE.Game.UIText.Draw(Text, new Point(Calc_X + (Calc_W / 2), Calc_Y + (Calc_H / 2) - (TextHeight / 2)), TextScale, ColorTemplate.TextColor, Font, true);
}
/// <summary>
/// A draggable box that can be moved across the screen.
/// Auto-snaps to the closest grid.
/// Basically your entire screen is an inventory.
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="w"></param>
/// <param name="h"></param>
/// <param name="text"></param>
public Draggable(int x, int y, int w, int h, string text)
{
X = x;
Y = y;
W = w;
H = h;
Text = text;
Draggable = true;
Hoverable = true;
}
}
public abstract class Drawable
{
// Position and Size Parameters Defined by the User
public int X { get; set; }
public int Y { get; set; }
public int W { get; set; }
public int H { get; set; }
// Customization
public ColorTemplate ColorTemplate = new ColorTemplate();
public RageFont Font = RageFont.ChaletComprimeCologne;
public Alignment TextAlignment = Alignment.Center;
public bool TextOutline = false;
public bool TextDropShadow = false;
// Text Elements and Such
public string Text = "";
public float TextScale = 0.6f;
public int TextHeight = 0;
public bool TextPrivate = false;
public int TextLeftPadding = 0;
// Scroll Functions
public int TextMaxLength = 45;
public int TextRenderStart = 0;
public int TextMaxLines = 5;
public List<string> TextScrollableList = new List<string>();
// Functional Parameters
public bool Hoverable = false;
public bool Clickable = false;
public bool Selectable = false;
public bool Scrollable = false;
public bool Typeable = false;
public bool Tabbable = false;
public bool Enterable = false;
public bool Draggable = false;
// Is?
public bool Hovered = false;
public bool Selected = false;
public bool Dragging = false;
// Function for this element. Requires Hoverable and Clickable.
public FunctionInvoker Function { get; set; }
// Calculated Variables
public int Calc_X { get; set; }
public int Calc_Y { get; set; }
public int Calc_W { get; set; }
public int Calc_H { get; set; }
// Draggable Calculated Variables
public int Drag_Calc_X { get; set; }
public int Drag_Calc_Y { get; set; }
public abstract void Draw();
public virtual void DrawInheritance()
{
Calculate();
if (Hoverable)
IsHovering();
if (Clickable && Hoverable && !Scrollable)
IsClicking();
if (Typeable && Selectable && Clickable)
IsTyping();
if (Scrollable && Hoverable && !Clickable && !Typeable)
IsScrolling();
if (Draggable && Hoverable)
IsDragging();
SetTextProperties();
}
public virtual void Calculate()
{
Vector2 Grid = RealScreenRes.GetGrid();
Calc_X = (Grid.X * X);
Calc_Y = (Grid.Y * Y);
Calc_W = (Grid.X * W) + Grid.X;
Calc_H = (Grid.Y * H) + Grid.Y;
TextHeight = Convert.ToInt32(720 * Ui.GetTextScaleHeight(TextScale, 4));
}
public virtual void IsHovering()
{
Vector2 Mouse = RealScreenRes.GetMouse();
if (Mouse.X > Calc_X && Mouse.X < (Calc_X + Calc_W) && Mouse.Y > Calc_Y && Mouse.Y < (Calc_Y + Calc_H))
{
Hovered = true;
return;
}
Hovered = false;
}
public virtual void IsClicking()
{
if (!Hovered)
return;
if (!RAGE.Game.Pad.IsDisabledControlJustPressed(0, 24))
return;
if (Function != null)
{
Function.Invoke();
RAGE.Game.Audio.PlaySoundFrontend(0, "NAV_UP_DOWN", "HUD_FREEMODE_SOUNDSET", true);
}
if (!Selectable)
return;
if (MenuBuilder.CurrentMenu.SelectedDrawable != null)
MenuBuilder.CurrentMenu.SelectedDrawable.Selected = false;
Selected = true;
MenuBuilder.CurrentMenu.SelectedDrawable = this;
}
public virtual void IsTyping()
{
if (!Selected)
return;
RAGE.Ui.DefaultWindow.ExecuteJs(@"
document.onkeypress = function(evt)
{
evt = event || window.event;
var charCode = evt.keyCode || evt.which;
if (charCode == 13) {
mp.trigger('InputKeyEnter');
return;
}
var charStr = String.fromCharCode(charCode);
mp.trigger('InputKeyPress', charStr);
};"
);
RAGE.Ui.DefaultWindow.ExecuteJs(@"
document.onkeydown = function(xyz)
{
xyz = event || window.event;
var charCode = xyz.keyCode;
if (charCode == 9)
{
mp.trigger('InputKeyTab');
return;
}
if (charCode != 8)
return;
mp.trigger('InputKeyBack');
};"
);
}
public virtual void IsScrolling()
{
string original = Text;
List<string> lines = Regex.Split(original, @"(.{1," + TextMaxLength + "})(?:\\s|$)")
.Where(x => x.Length > 0)
.ToList();
bool updatetext = false;
if (RAGE.Game.Pad.IsDisabledControlJustPressed(0, 15) && Hovered)
{
TextRenderStart -= 1;
if (TextRenderStart <= 0)
TextRenderStart = 0;
updatetext = true;
}
if (RAGE.Game.Pad.IsDisabledControlJustPressed(0, 16) && Hovered)
{
TextRenderStart += 1;
if (TextRenderStart > lines.Count)
TextRenderStart = lines.Count - 1;
updatetext = true;
}
if (!updatetext && TextScrollableList.Count != 0)
return;
TextScrollableList = new List<string>();
int RenderCount = 0;
if (TextRenderStart > lines.Count)
TextRenderStart = lines.Count - 1;
for (int i = TextRenderStart; i < lines.Count; i++)
{
if (i > lines.Count)
i = lines.Count - 1;
if (RenderCount > TextMaxLines)
break;
if (i != 0)
{
string newlines = "";
for (int x = 0; x < i - TextRenderStart; x++)
{
newlines += "~n~";
}
TextScrollableList.Add(newlines + lines[i]);
RenderCount++;
continue;
}
TextScrollableList.Add(lines[i]);
RenderCount++;
}
}
public virtual void IsDragging()
{
if (RAGE.Game.Pad.IsDisabledControlJustReleased(0, 24) && Dragging)
{
Dragging = false;
Vector2 result = RealScreenRes.ClosestGridToMouse();
X = result.X;
Y = result.Y;
Drag_Calc_X = result.X;
Drag_Calc_Y = result.Y;
return;
}
// Dragging
if (RAGE.Game.Pad.IsDisabledControlPressed(0, 24))
{
if (Hovered && !Dragging)
{
Dragging = true;
Vector2 mouse = RealScreenRes.GetMouse();
Drag_Calc_X = mouse.X - (Calc_W / 2);
Drag_Calc_Y = mouse.Y - (Calc_H / 2);
}
if (!Hovered && Dragging)
{
Dragging = true;
Vector2 mouse = RealScreenRes.GetMouse();
Drag_Calc_X = mouse.X - (Calc_W / 2);
Drag_Calc_Y = mouse.Y - (Calc_H / 2);
}
return;
}
}
public virtual void SetTextProperties()
{
if (TextOutline)
Ui.SetTextOutline();
if (TextDropShadow)
Ui.SetTextDropShadow();
}
}
public class FunctionInvoker
{
public string FunctionName { get; set; }
public object[] Arguments { get; set; }
public FunctionInvoker(string functionName, params object[] args)
{
FunctionName = functionName;
Arguments = args;
}
public void Invoke()
{
if (Arguments != null)
{
Events.CallLocal(FunctionName, Arguments);
return;
}
Events.CallLocal(FunctionName);
}
}
public enum Alignment
{
Center = 0,
Left = 1
}
public class ColorTemplate
{
public Color Color = Color.FromArgb(255, 76, 86, 97);
public Color ColorHover = Color.FromArgb(255, 147, 168, 172);
public Color TextColor = Color.FromArgb(255, 255, 255, 255);
public Color TextHoverColor = Color.FromArgb(255, 255, 255, 255);
public Color SelectedColor = Color.FromArgb(100, 255, 255, 255);
public Color UnSelectedColor = Color.FromArgb(25, 255, 255, 255);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment