EZUI Customizable Options Screen
Module — 2 files
These files compile together (same module folder).
ez_ui_optionsscreen.verse
<#
A basic options screen suitable for triggering devices via a
tabbed menu and is easily configured within the creative
device's custom properties
#>
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
#imports
using { /UnrealEngine.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Verse.org/Colors }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Verse.org/Assets }
using {EZUI}
EZ_OptionsScreen:= class ()
{
EZ_LinkedCreativeDevice<public> : ez_ui_optionsscreen # a backreference to the device that created this EZUI class
EZ_LinkedPlayer<public> : player # the player linked to this instance of the EZUI class
EZ_UI_ID<public> : int # used to control what UI is initialised by the class
EZ_OptionsButtonArray<public> : []EZ_OptionsButton # all options buttons that form the tabbed menu
EZ_OptionsTabs_HorizonalPos <public> : type{_X:float where -900.000000 <= _X, _X <= 900.000000}
EZ_OptionsTabs_VerticalPos <public> : type{_X:float where -900.000000 <= _X, _X <= 900.000000}
EZ_OptionsTab_Padding <public> : float
EZ_OptionsTab_Distribution <public> : float
EZ_OptionsTab_Colour <public> : color
EZ_OptionsTab_Opacity <public> : type {_X:float where 0.000000 <= _X, _X <= 1.000000}
EZ_OptionsTab_OutlineColour <public> : color
EZ_OptionsTab_LineThickness <public> : float
EZ_OptionsTab_OutlineOpacity <public> : type {_X:float where 0.000000 <= _X, _X <= 1.000000}
EZ_OptionsTab_Size <public> : vector2
EZ_SubOptionsPage_HorizonalPos <public> : type{_X:float where -900.000000 <= _X, _X <= 900.000000}
EZ_SubOptionsPage_VerticalPos <public> : type{_X:float where -900.000000 <= _X, _X <= 900.000000}
EZ_SubOptionsElement_Padding <public> : float
EZ_SubOptionsElement_Distribution <public> : float
EZ_SubOptionsElement_Colour <public> : color
EZ_SubOptionsElement_Opacity <public> : type {_X:float where 0.000000 <= _X, _X <= 1.000000}
EZ_SubOptionsElement_OutlineColour <public> : color
EZ_SubOptionsElement_OutlineOpacity <public> : type {_X:float where 0.000000 <= _X, _X <= 1.000000}
EZ_SubOptionsElement_Size <public> : vector2
EZ_SubOptionsElement_LineThickness <public> : float
EZ_UIBackground_Colour <public> : color
EZ_OptionsDistribution : float = 0.5
EZ_SubOptionsPadding : margin = margin{Top :=2.0, Left := 2.0, Right := 2.0, Bottom := 2.0}
EZ_SubOptionsDistribution : float = 0.25
var IsInitialized<private> : logic = false # a lock to prevent recreating the canvas after the first execution of Initialize()
var MainCanvas<private> : canvas = canvas{} # the canvas that holds all the screen elements and will be added to the PlayerUI
var OptionsButtonSlotArray : []stack_box_slot = array{} # a list of all slots created for the tabbed menu
var OptionsButtonsArray : []widget = array{} # a list of all buttons created for the tabbed menu
var SubOptionsSlotArray : []stack_box_slot = array{} # a list of all sub options slots for the currently selected tab
var SubOptionsElementArray : []widget = array{} # a list of all sub options elements for the currently selected tab
var SubOptionsButtonArray : []widget = array{} # a list of all sub options buttons
ColorBlock<private> : color_block = color_block
{
DefaultColor := color{R := 0.0, G := 0.0, B := 0.0}
DefaultOpacity := 0.5
DefaultDesiredSize := vector2{X := 1920.0, Y := 1080.0}
}
OptionsButtonsWrapper <private> : stack_box = stack_box { Orientation := orientation.Horizontal, Slots := array{} } # the tabbed menu bar
SubOptionsWrapper <private> : stack_box = stack_box { Orientation := orientation.Vertical, Slots := array{} } # contains the current tab's page elements
var SubOptionsButtonToTriggerMap <private> : [widget]trigger_device = map{} # suboption button / output trigger
var OptionsButtonToClassMap <private> : [widget]EZ_OptionsButton = map{} # option button / options class
var SubOptionsButtonToClassMap <private> : [widget]EZ_SubOptionsUI = map{} # option button / suboptions UI class
# do all the things
Ini<public>() : void =
if (IsInitialized?):
return
if(EZ_UI_ID = 1):
BuildOptionsScreen()
UpdateOptionsButtons()
#.........................
OpenUI<public>() : void =
if (PlayerUI := GetPlayerUI[EZ_LinkedPlayer]):
PlayerUI.AddWidget(GetMainCanvas(), player_ui_slot{InputMode := ui_input_mode.All}) #all canvas and content interactions active
# closes specific UI using individual behavior
CloseUI<public>(PlayerUI: player_ui, ActiveWidget : widget)<suspends> : void =
if (EZ_UI_ID = 1):
PlayerUI.RemoveWidget(GetMainCanvas())
#.........................
GetMainCanvas<public>() : canvas = MainCanvas
GetOptionsButtonsWrapper <public> () : stack_box = OptionsButtonsWrapper
GetOptionsPageWrapper <public> () : stack_box = SubOptionsWrapper
#.........................
CloseUIButtonClick<private>(WidgetEvent : widget_message) : void =
if (PlayerUI := GetPlayerUI[WidgetEvent.Player]):
spawn{CloseUI(PlayerUI,WidgetEvent.Source)}
#.........................
OnOptionsButtonClicked <private>(WidgetEvent : widget_message) : void =
MyOptionsButtonClass := GetOptionsButtonClass(WidgetEvent.Source)
if (MyOptionsButtonClass.IsExitButton?):
CloseUIButtonClick(WidgetEvent)
EZ_LinkedCreativeDevice.CloseUITrigger.Trigger(WidgetEvent.Player)
else:
UpdateSubOptionsPage (WidgetEvent.Source)
#.........................
# handles the creation of the tabbed options menu
UpdateOptionsButtons <private> () : void =
for(ThisOptionsButtonClass : EZ_OptionsButtonArray):
NewButton := button_quiet {DefaultText := EZUI.TextToMsg(ThisOptionsButtonClass.OptionsButtonText)}
NewButton.SetEnabled(false) # start disabled
NewButton.OnClick().Subscribe(OnOptionsButtonClicked)
# updates the list of all slots, used to build the UI
NewButtonSlot := stack_box_slot
{
Padding := margin{Top :=EZ_OptionsTab_Padding, Left := EZ_OptionsTab_Padding, Right := EZ_OptionsTab_Padding, Bottom := EZ_OptionsTab_Padding}
Distribution := option{EZ_OptionsTab_Distribution}
Widget := overlay:
Slots := array:
overlay_slot:
Widget := color_block # UI outline
{
DefaultColor := EZ_OptionsTab_OutlineColour
DefaultOpacity := EZ_OptionsTab_OutlineOpacity
DefaultDesiredSize := vector2{X := EZ_OptionsTab_Size.X + (EZ_OptionsTab_LineThickness*2), Y := EZ_OptionsTab_Size.Y + (EZ_OptionsTab_LineThickness*2)}
}
overlay_slot:
Widget := color_block # UI main
{
DefaultColor := EZ_OptionsTab_Colour
DefaultOpacity := EZ_OptionsTab_Opacity
DefaultDesiredSize := EZ_OptionsTab_Size
}
overlay_slot:
Widget := NewButton
}
set OptionsButtonSlotArray += array{NewButtonSlot}
# updates the list of all created buttons, used to find the class which created the button as they will share the same index
set OptionsButtonsArray += array{NewButton}
if(set OptionsButtonToClassMap[NewButton] = ThisOptionsButtonClass): #instance specific button <> class map
# adds the new slot and button to the wrapper
OptionsButtonsWrapper.AddWidget(NewButtonSlot)
ToggleUIInteractions() #enable
# use first options button as the landing page
if (DefaultPage := OptionsButtonsArray[0]):
UpdateSubOptionsPage(DefaultPage)
#.........................
# each button has a class which created it, this function takes several steps to display all sub options for the clicked button to create a tabbed menu
UpdateSubOptionsPage <public> ( ActiveWidget : widget) : void =
ToggleUIInteractions() # disable
NewOutputMap : [widget]trigger_device = map{}
spawn{ClearSubOptionsPage()} # done as async to hopefully speed up the UI
set SubOptionsButtonToTriggerMap = NewOutputMap # clears old output map
var NewSubOptionsArray : []EZ_SubOptionsUI = array{}
for (i := 0..OptionsButtonsArray.Length -1 ): #1. searches the list of all created options buttons to find the one which was clicked
if (OptionsButtonsArray[i] = ActiveWidget):
Print("Button found @ {i}")
if(MyOptionButtonClass := EZ_OptionsButtonArray[i]): #2. since the class that created the button shares the same index we can do a reverse lookup
set NewSubOptionsArray = MyOptionButtonClass.SubOptionsUIArray #3. create a new array for the suboptions page from this paired class
# rebuilds the page
for (ThisSubOption : NewSubOptionsArray):
NewSubOptionButton := button_quiet{}
ThisSubOption.SetInitialOptionState(NewSubOptionButton)
NewSubOptionButton.SetEnabled(false) # start disabled
NewSubOptionButton.OnClick().Subscribe(ThisSubOption.OnSubOptionsButtonClicked) # each button class instance handles it's own events
NewSubOptionTextBlock := text_block
{
DefaultText := EZUI.TextToMsg(ThisSubOption.SubOptionText)
DefaultOverflowPolicy :=text_overflow_policy.Ellipsis
DefaultTextColor := ThisSubOption.SubOptionTextColour
}
<#
UI elements within a stackbox don't allow you to resize them, offset or inhert from their
parent container so an overlay is needed to control the contents. it also allows for
more complex multi-widget layouts
#>
NewSubOptionsElement := overlay
{
Slots := array:
overlay_slot:
Widget := color_block # UI outline
{
DefaultColor := EZ_SubOptionsElement_OutlineColour
DefaultOpacity := EZ_SubOptionsElement_OutlineOpacity
DefaultDesiredSize := vector2{X := EZ_SubOptionsElement_Size.X + (EZ_SubOptionsElement_LineThickness*2), Y := EZ_SubOptionsElement_Size.Y + (EZ_SubOptionsElement_LineThickness*2)}
}
overlay_slot:
Widget := color_block # UI main
{
DefaultColor := EZ_SubOptionsElement_Colour
DefaultOpacity := EZ_SubOptionsElement_Opacity
DefaultDesiredSize := EZ_SubOptionsElement_Size
}
overlay_slot:
Widget := canvas:
Slots := array:
canvas_slot
{
Anchors := anchors{Minimum := vector2{X := 0.5 , Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
Offsets := margin{Top := 0.0, Left := -250.0, Right := 0.0, Bottom := 0.0}
Alignment := vector2{X := 0.5, Y := 0.5}
ZOrder := 0
SizeToContent := true
Widget := NewSubOptionTextBlock
}
overlay_slot:
Widget := canvas:
Slots := array:
canvas_slot
{
Anchors := anchors{Minimum := vector2{X := 0.5 , Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
Offsets := margin{Top := 0.0, Left := 375.0, Right := 0.0, Bottom := 0.0}
Alignment := vector2{X := 0.5, Y := 0.5}
ZOrder := 0
SizeToContent := true
Widget := NewSubOptionButton
}
}
NewSubOptionsSlot := stack_box_slot
{
Padding := margin{Top := EZ_SubOptionsElement_Padding, Left := EZ_SubOptionsElement_Padding, Right := EZ_SubOptionsElement_Padding, Bottom := EZ_SubOptionsElement_Padding}
Distribution := option{EZ_SubOptionsElement_Distribution}
Widget := NewSubOptionsElement
}
set SubOptionsSlotArray += array{NewSubOptionsSlot} # replace the old suboptions slots array
set SubOptionsElementArray += array{NewSubOptionsElement} # replace the old suboptions element array
set SubOptionsButtonArray += array{NewSubOptionButton} # replace the old suboptions button array
if(set SubOptionsButtonToTriggerMap[NewSubOptionButton] = ThisSubOption.OutputTrigger):
SubOptionsWrapper.AddWidget(NewSubOptionsSlot) # add the new suboptions elements to the wrapper
ToggleUIInteractions() # enable
#.........................
ClearSubOptionsPage () <suspends> : void =
for (i := 0..SubOptionsElementArray.Length -1 ):
if(TargetWidget := SubOptionsElementArray[i]):
GetOptionsPageWrapper().RemoveWidget(TargetWidget)
#Print ("Removed Widget at index {i}")
#.........................
ToggleUIInteractions() : void =
# options buttons
for (b: OptionsButtonsArray):
IsEnabled := b.IsEnabled()
if (IsEnabled?):
b.SetEnabled(false)
else:
b.SetEnabled(true)
# sub options buttons
for (b : SubOptionsButtonArray):
IsEnabled := b.IsEnabled()
if (IsEnabled?):
b.SetEnabled(false)
else:
b.SetEnabled(true)
#.........................
GetOptionsButtonClass(TargetWidget : widget) : EZ_OptionsButton =
var MyClass : EZ_OptionsButton = EZ_OptionsButton{}
for (Widget-> Class : OptionsButtonToClassMap):
if(Widget = TargetWidget):
set MyClass = Class
return MyClass
GetSubOptionsButtonClass() : EZ_SubOptionsUI=
x : EZ_SubOptionsUI = EZ_SubOptionsUI{}
return x
#.........................
# Builds a canvas
BuildOptionsScreen <private>() : void =
ColorBlock.SetColor (EZ_UIBackground_Colour)
set MainCanvas = canvas
{
Slots := array
{
canvas_slot #background
{
Anchors := anchors{Minimum := vector2{X := 0.5 , Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
Alignment := vector2{X := 0.5, Y := 0.5}
ZOrder := 0
SizeToContent := true
Widget := ColorBlock
}
canvas_slot #tabbed menu bar wrapper
{
Anchors := anchors{Minimum := vector2{X := 0.5 , Y := 0.5}, Maximum := vector2{X := 0.5, Y := 0.5}}
Offsets := margin{Top := EZ_OptionsTabs_VerticalPos, Left := EZ_OptionsTabs_HorizonalPos, Right := 0.0, Bottom := 0.0}
Alignment := vector2{X := 0.5, Y := 0.5}
ZOrder := 2
SizeToContent := true
Widget := stack_box:
Orientation := orientation.Horizontal
Slots := array:
stack_box_slot:
HorizontalAlignment := horizontal_alignment.Center
VerticalAlignment := vertical_alignment.Center
Padding := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
Distribution := option{EZ_OptionsTab_Distribution}
Widget := OptionsButtonsWrapper
}
canvas_slot # suboptions page wrapper
{
Anchors := anchors{Minimum := vector2{X := 0.0 , Y := 0.0}, Maximum := vector2{X := 1.0, Y := 1.0}}
Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.5, Bottom := 0.0}
Alignment := vector2{X := 0.5, Y := 0.5}
ZOrder := 1
SizeToContent := false
Widget := canvas:
Slots := array:
canvas_slot:
Anchors := anchors{Minimum := vector2{X := 0.0 , Y := 0.0}, Maximum := vector2{X := 1.0, Y := 1.0}}
Offsets := margin{Top := 350.0, Left := 0.0, Right := 0.0, Bottom := 350.0}
Alignment := vector2{X := 0.5, Y := 0.5}
ZOrder := 1
SizeToContent := true
Widget := SubOptionsWrapper
}
}
}
set IsInitialized = true # Lock the inialization state to prevent from running this function again
#.........................
} # END EZ_OptionsScreen------------------------------------------------------------------------------------------------------------------------------------
EZ_OptionsButton := class<concrete>()
{
@editable var OptionsButtonText : string = "OptionMenuText"
@editable IsExitButton : logic = false
@editable SubOptionsUIArray : []EZ_SubOptionsUI = array{}
} # END EZ_OptionsButton ------------------------------------------------------------------------------------------------------------------------------------
EZ_SubOptionsUI := class<concrete>()
{
@editable var SubOptionsButtonText1 : string = "ButtonText1"
@editable var SubOptionsButtonText2 : string = "ButtonText2"
@editable var SubOptionText : string = "SubOptionText"
@editable var SubOptionTextColour : color =color{R := 0.0, G := 0.0, B := 0.1}
@editable OutputTrigger : trigger_device = trigger_device {}
@editable var IsEnabled : logic = false
OnSubOptionsButtonClicked (WidgetEvent : widget_message) : void =
if (ButtonWidget := text_button_base[WidgetEvent.Source]):
if (IsEnabled?):
ButtonWidget.SetText(EZUI.TextToMsg(SubOptionsButtonText2))
set IsEnabled = false
else:
ButtonWidget.SetText(EZUI.TextToMsg(SubOptionsButtonText1))
set IsEnabled = true
OutputTrigger.Trigger(WidgetEvent.Player)
SetInitialOptionState ( MyButton : text_button_base) : void =
if ( IsEnabled ?):
MyButton.SetText(EZUI.TextToMsg(SubOptionsButtonText1))
else:
MyButton.SetText(EZUI.TextToMsg(SubOptionsButtonText2))
} # END EZ_SubOptionsUI ------------------------------------------------------------------------------------------------------------------------------------
ez_ui_optionsscreen := class(creative_device):
@editable ShowUITrigger : trigger_device = trigger_device {}
@editable CloseUITrigger : trigger_device = trigger_device {} #added based upon feedback by jackherrer1113 Nov'23
@editable OptionsTabs_HorizonalPos : type{_X:float where -900.000000 <= _X, _X <= 900.000000} = 0.0
@editable OptionsTabs_VerticalPos : type{_X:float where -500.000000 <= _X, _X <= 500.000000} = -250.0
@editable OptionsTab_Padding : float = 2.0
@editable OptionsTab_Distribution : type{_X:float where 0.000000 <= _X, _X <= 1.000000} = 0.25
@editable OptionsTab_Colour : color = color{R := 0.5, G := 0.5, B := 0.7}
@editable OptionsTab_Opacity : type{_X:float where 0.000000 <= _X, _X <= 1.000000} = 1.0
@editable OptionsTab_OutlineColour : color = color{R := 0.0, G := 0.0, B := 0.0}
@editable OptionsTab_LineThickness : float = 5.0
@editable OptionsTab_OutlineOpacity : type{_X:float where 0.000000 <= _X, _X <= 1.000000} = 0.75
@editable OptionsTab_Size : vector2 = vector2{X := 250.0, Y := 50.0}
@editable SubOptionsPage_HorizonalPos : type{_X:float where -900.000000 <= _X, _X <= 900.000000} = 0.0
@editable SubOptionsPage_VerticalPos : type{_X:float where -500.000000 <= _X, _X <= 500.000000} = 0.0
@editable SubOptionsElement_Padding : float = 2.0
@editable SubOptionsElement_Distribution : type{_X:float where 0.000000 <= _X, _X <= 1.000000} = 0.25
@editable SubOptionsElement_Colour : color = color{R := 0.5, G := 0.5, B := 0.65}
@editable SubOptionsElement_Opacity : type{_X:float where 0.000000 <= _X, _X <= 1.000000} = 1.0
@editable SubOptionsElement_OutlineColour : color = color{R := 0.0, G := 0.0, B := 0.0}
@editable SubOptionsElement_OutlineOpacity : type{_X:float where 0.000000 <= _X, _X <= 1.000000} = 0.75
@editable SubOptionsElement_Size : vector2 = vector2{X := 1000.0, Y := 50.0}
@editable SubOptionsElement_LineThickness : float = 3.0
@editable UIBackground_Colour : color =color{R := 0.0, G := 0.0, B := 0.0}
@editable var OptionsButtonArray : []EZ_OptionsButton = array{}
# UI regisitration
var PlayerToUIMap : [player]EZ_OptionsScreen = map{} #[playerdata]/ez_ui class
OnBegin<override>()<suspends> : void =
ShowUITrigger.TriggeredEvent.Subscribe(OpenUI)
# all the steps needed to open the Pop Up
OpenUI<private>(MaybePlayer : ?agent) : void =
{
# check that the event was triggered by a player and stop if it wasn't
Result := EZUI.IsAPlayer(MaybePlayer,Self)
if (Result = false):
return
#.........................
var UI_ID : int = 1
if(PlayerAgent := MaybePlayer?): #set the typing to agent from ?agent
if ( InteractingPlayer := player[PlayerAgent] )
{
var OptPlayerUI :?EZ_OptionsScreen = false
if (ThisPlayerUI := PlayerToUIMap[InteractingPlayer])
{ set OptPlayerUI = option{ThisPlayerUI} }
else if (ThisPlayerUI := set PlayerToUIMap[InteractingPlayer] = EZ_OptionsScreen
#everything that is passed to the EZUI class
{
EZ_LinkedPlayer := InteractingPlayer,
EZ_LinkedCreativeDevice := Self,
EZ_UI_ID := UI_ID,
EZ_OptionsButtonArray := OptionsButtonArray,
EZ_OptionsTabs_HorizonalPos := OptionsTabs_HorizonalPos,
EZ_OptionsTabs_VerticalPos := OptionsTabs_VerticalPos,
EZ_OptionsTab_Padding := OptionsTab_Padding,
EZ_OptionsTab_Distribution := OptionsTab_Distribution,
EZ_OptionsTab_Colour := OptionsTab_Colour,
EZ_OptionsTab_Opacity := OptionsTab_Opacity,
EZ_OptionsTab_OutlineColour := OptionsTab_OutlineColour,
EZ_OptionsTab_LineThickness := OptionsTab_LineThickness,
EZ_OptionsTab_OutlineOpacity := OptionsTab_OutlineOpacity,
EZ_OptionsTab_Size := OptionsTab_Size,
EZ_SubOptionsPage_HorizonalPos := SubOptionsPage_HorizonalPos,
EZ_SubOptionsPage_VerticalPos := SubOptionsPage_VerticalPos,
EZ_SubOptionsElement_Padding := SubOptionsElement_Padding,
EZ_SubOptionsElement_Distribution := SubOptionsElement_Distribution,
EZ_SubOptionsElement_Colour:= SubOptionsElement_Colour,
EZ_SubOptionsElement_Opacity := SubOptionsElement_Opacity,
EZ_SubOptionsElement_OutlineColour := SubOptionsElement_OutlineColour,
EZ_SubOptionsElement_OutlineOpacity := SubOptionsElement_OutlineOpacity,
EZ_SubOptionsElement_Size := SubOptionsElement_Size,
EZ_SubOptionsElement_LineThickness := SubOptionsElement_LineThickness,
EZ_UIBackground_Colour := UIBackground_Colour
}
)
{ ThisPlayerUI.Ini(), set OptPlayerUI = option{ThisPlayerUI} }
# Try to open the Interface for the player
if (PlayerUI := OptPlayerUI?):
PlayerUI.OpenUI()
else
{ Print("Error when getting saved or creating ThisPlayerUI") }
}
#.........................
}
Sign in to download module
Copy-paste each file above is always free.