Repository: coding-jackalope/Slab Branch: master Commit: 9b2e3e840c90 Files: 49 Total size: 507.7 KB Directory structure: gitextract__a007ahg/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── API.lua ├── Internal/ │ ├── Core/ │ │ ├── Config.lua │ │ ├── Cursor.lua │ │ ├── DrawCommands.lua │ │ ├── FileSystem.lua │ │ ├── IdCache.lua │ │ ├── Messages.lua │ │ ├── Scale.lua │ │ ├── Stats.lua │ │ ├── TablePool.lua │ │ └── Utility.lua │ ├── Input/ │ │ ├── Common.lua │ │ ├── Keyboard.lua │ │ └── Mouse.lua │ ├── Resources/ │ │ └── Styles/ │ │ ├── Dark.style │ │ └── Light.style │ └── UI/ │ ├── Button.lua │ ├── CheckBox.lua │ ├── ColorPicker.lua │ ├── ComboBox.lua │ ├── Dialog.lua │ ├── Dock.lua │ ├── Image.lua │ ├── Input.lua │ ├── LayoutManager.lua │ ├── ListBox.lua │ ├── Menu.lua │ ├── MenuBar.lua │ ├── MenuState.lua │ ├── Region.lua │ ├── Separator.lua │ ├── Shape.lua │ ├── Text.lua │ ├── Tooltip.lua │ ├── Tree.lua │ └── Window.lua ├── LICENSE ├── README.md ├── Slab.lua ├── SlabDebug.lua ├── SlabDefinition.lua ├── SlabTest.lua ├── Style.lua ├── changelog.txt ├── conf.lua ├── init.lua └── main.lua ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms custom: https://www.paypal.com/donate/?hosted_button_id=PHU9JP36QQYG2 ================================================ FILE: .gitignore ================================================ Slab.ini .luarc.json ================================================ FILE: API.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] if SLAB_PATH == nil then SLAB_PATH = (...):match("(.-)[^%.]+$") end SLAB_FILE_PATH = debug.getinfo(1, 'S').source:match("^@(.+)/") SLAB_FILE_PATH = SLAB_FILE_PATH == nil and "" or SLAB_FILE_PATH local StatsData = {} local PrevStatsData = {} local Button = require(SLAB_PATH .. '.Internal.UI.Button') local CheckBox = require(SLAB_PATH .. '.Internal.UI.CheckBox') local ColorPicker = require(SLAB_PATH .. '.Internal.UI.ColorPicker') local ComboBox = require(SLAB_PATH .. '.Internal.UI.ComboBox') local Config = require(SLAB_PATH .. '.Internal.Core.Config') local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local Dialog = require(SLAB_PATH .. '.Internal.UI.Dialog') local Dock = require(SLAB_PATH .. '.Internal.UI.Dock') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local FileSystem = require(SLAB_PATH .. '.Internal.Core.FileSystem') local Image = require(SLAB_PATH .. '.Internal.UI.Image') local Input = require(SLAB_PATH .. '.Internal.UI.Input') local Keyboard = require(SLAB_PATH .. '.Internal.Input.Keyboard') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local ListBox = require(SLAB_PATH .. '.Internal.UI.ListBox') local Messages = require(SLAB_PATH .. '.Internal.Core.Messages') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Menu = require(SLAB_PATH .. '.Internal.UI.Menu') local MenuState = require(SLAB_PATH .. '.Internal.UI.MenuState') local MenuBar = require(SLAB_PATH .. '.Internal.UI.MenuBar') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Separator = require(SLAB_PATH .. '.Internal.UI.Separator') local Shape = require(SLAB_PATH .. '.Internal.UI.Shape') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Tree = require(SLAB_PATH .. '.Internal.UI.Tree') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Window = require(SLAB_PATH .. '.Internal.UI.Window') --[[ Slab Slab is an immediate mode GUI toolkit for the Love 2D framework. This library is designed to allow users to easily add this library to their existing Love 2D projects and quickly create tools to enable them to iterate on their ideas quickly. The user should be able to utilize this library with minimal integration steps and is completely written in Lua and utilizes the Love 2D API. No compiled binaries are required and the user will have access to the source so that they may make adjustments that meet the needs of their own projects and tools. Refer to main.lua and SlabTest.lua for example usage of this library. Supported Version: 11.3.0 API: Initialize GetVersion GetLoveVersion Update Draw SetINIStatePath GetINIStatePath SetVerbose GetMessages Style: GetStyle PushFont PopFont Window: BeginWindow EndWindow GetWindowPosition GetWindowSize GetWindowContentSize GetWindowActiveSize IsWindowAppearing PushID PopID Menu: BeginMainMenuBar EndMainMenuBar BeginMenuBar EndMenuBar BeginMenu EndMenu BeginContextMenuItem BeginContextMenuWindow EndContextMenu MenuItem MenuItemChecked Separator Button RadioButton Text TextSelectable Textf GetTextSize GetTextWidth GetTextHeight CheckBox Input InputNumberDrag InputNumberSlider GetInputText GetInputNumber GetInputCursorPos IsInputFocused IsAnyInputFocused SetInputFocus SetInputCursorPos SetInputCursorPosLine BeginTree EndTree BeginComboBox EndComboBox Image Cursor: SameLine NewLine SetCursorPos GetCursorPos Indent Unindent Properties ListBox: BeginListBox EndListBox BeginListBoxItem IsListBoxItemClicked EndListBoxItem Dialog: OpenDialog BeginDialog EndDialog CloseDialog MessageBox FileDialog ColorPicker Mouse: IsMouseDown IsMouseClicked IsMouseReleased IsMouseDoubleClicked IsMouseDragging GetMousePosition GetMousePositionWindow GetMouseDelta SetCustomMouseCursor ClearCustomMouseCursor Control: IsControlHovered IsControlClicked GetControlSize IsVoidHovered IsVoidClicked Keyboard: IsKeyDown IsKeyPressed IsKeyReleased Shape: Rectangle Circle Triangle Line Curve GetCurveControlPointCount GetCurveControlPoint EvaluateCurve EvaluateCurveMouse Polygon Stats: BeginStat EndStat EnableStats IsStatsEnabled FlushStats Layout: BeginLayout EndLayout SetLayoutColumn GetLayoutSize Scroll: SetScrollSpeed GetScrollSpeed Shader: PushShader PopShader Dock: EnableDocks DisableDocks SetDockOptions --]] local Slab = {} -- Slab version numbers. local Version_Major = 0 local Version_Minor = 9 local Version_Revision = 0 local FrameStatHandle = nil -- The path to save the UI state to a file. This will default to the save directory. local INIStatePath = "Slab.ini" local IsDefault = true local QuitFn = nil local Verbose = false local Initialized = false local DontInterceptEventHandlers = false local ModifyCursor = true local function LoadState() if INIStatePath == nil then return end local Result, Error = Config.LoadFile(INIStatePath, IsDefault) if Result ~= nil then Dock.Load(Result) Tree.Load(Result) Window.Load(Result) end if Verbose then print("Load INI file:", INIStatePath, "Error:", Error) end end local function SaveState() if INIStatePath == nil then return end local Table = {} Dock.Save(Table) Tree.Save(Table) Window.Save(Table) local Result, Error = Config.Save(INIStatePath, Table, IsDefault) if Verbose then print("Save INI file:", INIStatePath, "Error:", Error) end end local function TextInput(Ch) Input.Text(Ch) if (not DontInterceptEventHandlers) and love.textinput ~= nil then love.textinput(Ch) end end local function WheelMoved(X, Y) Window.WheelMoved(X, Y) if (not DontInterceptEventHandlers) and love.wheelmoved ~= nil then love.wheelmoved(X, Y) end end local function OnQuit() SaveState() if QuitFn ~= nil then QuitFn() end end --[[ Event forwarding --]] Slab.OnTextInput = TextInput; Slab.OnWheelMoved = WheelMoved; Slab.OnQuit = OnQuit; Slab.OnKeyPressed = Keyboard.OnKeyPressed; Slab.OnKeyReleased = Keyboard.OnKeyReleased; Slab.OnMouseMoved = Mouse.OnMouseMoved Slab.OnMousePressed = Mouse.OnMousePressed; Slab.OnMouseReleased = Mouse.OnMouseReleased; --[[ Initialize Initializes Slab and hooks into the required events. This function should be called in love.load. args: [Table] The list of parameters passed in by the user on the command-line. This should be passed in from love.load function. Below is a list of arguments available to modify Slab: NoMessages: [String] Disables the messaging system that warns developers of any changes in the API. NoDocks: [String] Disables all docks. NoCursor: [String] Disables modifying the cursor Return: None. --]] function Slab.Initialize(args, dontInterceptEventHandlers) if Initialized then return end DontInterceptEventHandlers = dontInterceptEventHandlers Style.API.Initialize() args = args or {} if type(args) == 'table' then for I, V in ipairs(args) do if string.lower(V) == 'nomessages' then Messages.SetEnabled(false) elseif string.lower(V) == 'nodocks' then Slab.DisableDocks({'Left', 'Right', 'Bottom'}) elseif string.lower(V) == 'nocursor' then ModifyCursor = false end end end if not dontInterceptEventHandlers then love.handlers['textinput'] = TextInput love.handlers['wheelmoved'] = WheelMoved -- In Love 11.3, overriding love.handlers['quit'] doesn't seem to affect the callback during shutdown. -- Storing and overriding love.quit manually will properly call Slab's callback. This function will call -- the stored function once Slab is finished with its process. QuitFn = love.quit love.quit = OnQuit end Keyboard.Initialize(args, dontInterceptEventHandlers) Mouse.Initialize(args, dontInterceptEventHandlers) LoadState() Initialized = true end --[[ GetVersion Retrieves the current version of Slab being used as a string. Return: [String] String of the current Slab version. --]] function Slab.GetVersion() return string.format("%d.%d.%d", Version_Major, Version_Minor, Version_Revision) end --[[ GetLoveVersion Retrieves the current version of Love being used as a string. Return: [String] String of the current Love version. --]] function Slab.GetLoveVersion() local Major, Minor, Revision, Codename = love.getVersion() return string.format("%d.%d.%d - %s", Major, Minor, Revision, Codename) end --[[ Update Updates the input state and states of various widgets. This function must be called every frame. This should be called before any Slab calls are made to ensure proper responses to Input are made. dt: [Number] The delta time for the frame. This should be passed in from love.update. Return: None. --]] function Slab.Update(dt) Stats.Reset(false) FrameStatHandle = Stats.Begin('Frame', 'Slab') local StatHandle = Stats.Begin('Update', 'Slab') Mouse.Update() Keyboard.Update() Input.Update(dt) DrawCommands.Reset() Window.Reset() LayoutManager.Validate() if MenuState.IsOpened then MenuState.WasOpened = MenuState.IsOpened if Mouse.IsClicked(1) then MenuState.RequestClose = true end end Stats.End(StatHandle) end --[[ Draw This function will execute all buffered draw calls from the various Slab calls made prior. This function should be called from love.draw and should be called at the very to ensure Slab is rendered above the user's workspace. Return: None. --]] function Slab.Draw() if Stats.IsEnabled() then PrevStatsData = love.graphics.getStats(PrevStatsData) end local StatHandle = Stats.Begin('Draw', 'Slab') Window.Validate() local MovingInstance = Window.GetMovingInstance() if MovingInstance ~= nil then Dock.DrawOverlay() Dock.SetPendingWindow(MovingInstance) else Dock.Commit() end if MenuState.RequestClose then Menu.Close() MenuBar.Clear() end if ModifyCursor then Mouse.Draw() end if Mouse.IsReleased(1) then Button.ClearClicked() end love.graphics.setColor(1, 1, 1, 1) love.graphics.push() love.graphics.origin() DrawCommands.Execute() love.graphics.pop() love.graphics.setColor(1, 1, 1, 1) Stats.End(StatHandle) -- Only call end if 'Update' was called and a valid handle was retrieved. This can happen for developers using a custom -- run function with a fixed update. if FrameStatHandle ~= nil then Stats.End(FrameStatHandle) FrameStatHandle = nil end if Stats.IsEnabled() then StatsData = love.graphics.getStats(StatsData) for k, v in pairs(StatsData) do StatsData[k] = v - PrevStatsData[k] end end end --[[ SetINIStatePath Sets the INI path to save the UI state. If nil, Slab will not save the state to disk. Return: None. --]] function Slab.SetINIStatePath(Path) INIStatePath = Path IsDefault = false end --[[ GetINIStatePath Gets the INI path to save the UI state. This value can be nil. Return: [String] The path on disk the UI state will be saved to. --]] function Slab.GetINIStatePath() return INIStatePath end --[[ SetVerbose Enable/Disables internal Slab logging. Could be useful for diagnosing problems that occur inside of Slab. IsVerbose: [Boolean] Flag to enable/disable verbose logging. Return: None. --]] function Slab.SetVerbose(IsVerbose) Verbose = (IsVerbose == nil or type(IsVerbose) ~= 'boolean') and false or IsVerbose end --[[ GetMessages Retrieves a list of existing messages that has been captured by Slab. Return: [Table] List of messages that have been broadcasted from Slab. --]] function Slab.GetMessages() return Messages.Get() end --[[ GetStyle Retrieve the style table associated with the current instance of Slab. This will allow the user to add custom styling to their controls. Return: [Table] The style table. --]] function Slab.GetStyle() return Style end --[[ SetScale Sets the rendering scale for the Slab context. scaleFactor: [number] The scale factor to use Return: None. --]] function Slab.SetScale(scaleFactor) Scale.SetScale(scaleFactor) end --[[ GetScale Retrieve the scale of the current Slab context. Return: [number] The current scale. --]] function Slab.GetScale() return Scale.GetScale() end --[[ PushFont Pushes a Love font object onto the font stack. All text rendering will use this font until PopFont is called. Font: [Object] The Love font object to use. Return: None. --]] function Slab.PushFont(Font) Style.API.PushFont(Font) end --[[ PopFont Pops the last font from the stack. Return: None. --]] function Slab.PopFont() Style.API.PopFont() end --[[ BeginWindow This function begins the process of drawing widgets to a window. This function must be followed up with an EndWindow call to ensure proper behavior of drawing windows. Id: [String] A unique string identifying this window in the project. Options: [Table] List of options that control how this window will behave. X: [Number] The X position to start rendering the window at. Y: [Number] The Y position to start rendering the window at. W: [Number] The starting width of the window. H: [Number] The starting height of the window. ContentW: [Number] The starting width of the content contained within this window. ContentH: [Number] The starting height of the content contained within this window. BgColor: [Table] The background color value for this window. Will use the default style WindowBackgroundColor if this is empty. Title: [String] The title to display for this window. If emtpy, no title bar will be rendered and the window will not be movable. TitleH: [Number] The height of the title bar. By default, this will be the height of the current font set in the style. If no title is set, this is ignored. TitleAlignX: [String] Horizontal alignment of the title. The available options are 'left', 'center', and 'right'. The default is 'center'. TitleAlignY: [String] Vertical alignment of the title. The available options are 'top', 'center', and 'bottom'. The default is 'center'. AllowMove: [Boolean] Controls whether the window is movable within the title bar area. The default value is true. AllowResize: [Boolean] Controls whether the window is resizable. The default value is true. AutoSizeWindow must be false for this to work. AllowFocus: [Boolean] Controls whether the window can be focused. The default value is true. Border: [Number] The value which controls how much empty space should be left between all sides of the window from the content. The default value is 4.0 NoOutline: [Boolean] Controls whether an outline should not be rendered. The default value is false. IsMenuBar: [Boolean] Controls whether if this window is a menu bar or not. This flag should be ignored and is used by the menu bar system. The default value is false. AutoSizeWindow: [Boolean] Automatically updates the window size to match the content size. The default value is true. AutoSizeWindowW: [Boolean] Automatically update the window width to match the content size. This value is taken from AutoSizeWindow by default. AutoSizeWindowH: [Boolean] Automatically update the window height to match the content size. This value is taken from AutoSizeWindow by default. AutoSizeContent: [Boolean] The content size of the window is automatically updated with each new widget. The default value is true. Layer: [String] The layer to which to draw this window. This is used internally and should be ignored by the user. ResetPosition: [Boolean] Determines if the window should reset any delta changes to its position. ResetSize: [Boolean] Determines if the window should reset any delta changes to its size. ResetContent: [Boolean] Determines if the window should reset any delta changes to its content size. ResetLayout: [Boolean] Will reset the position, size, and content. Short hand for the above 3 flags. SizerFilter: [Table] Specifies what sizers are enabled for the window. If nothing is specified, all sizers are available. The values can be: NW, NE, SW, SE, N, S, E, W CanObstruct: [Boolean] Sets whether this window is considered for obstruction of other windows and their controls. The default value is true. Rounding: [Number] Amount of rounding to apply to the corners of the window. IsOpen: [Boolean] Determines if the window is open. If this value exists within the options, a close button will appear in the corner of the window and is updated when this button is pressed to reflect the new open state of this window. NoSavedSettings: [Boolean] Flag to disable saving this window's settings to the state INI file. ConstrainPosition: [Boolean] Flag to constrain the position of the window to the bounds of the viewport. ShowMinimize: [Boolean] Flag to show a minimize button in the title bar of the window. Default is `true`. ShowScrollbarX: [Boolean] Flag to show the horizontal scrollbar regardless of the window and content internal state. Default is `false` ShowScrollbarY: [Boolean] Flag to show the vertical scrollbar regardless of the window and content internal state. Default is `false` Return: [Boolean] The open state of this window. Useful for simplifying API calls by storing the result in a flag instead of a table. EndWindow must still be called regardless of the result for this value. --]] function Slab.BeginWindow(Id, Options) return Window.Begin(Id, Options) end --[[ EndWindow This function must be called after a BeginWindow and associated widget calls. If the user fails to call this, an assertion will be thrown to alert the user. Return: None. --]] function Slab.EndWindow() Window.End() end --[[ GetWindowPosition Retrieves the active window's position. Return: [Number], [Number] The X and Y position of the active window. --]] function Slab.GetWindowPosition() return Window.GetPosition() end --[[ GetWindowSize Retrieves the active window's size. Return: [Number], [Number] The width and height of the active window. --]] function Slab.GetWindowSize() return Window.GetSize() end --[[ GetWindowContentSize Retrieves the active window's content size. Return: [Number], [Number] The width and height of the active window content. --]] function Slab.GetWindowContentSize() return Window.GetContentSize() end --[[ GetWindowActiveSize Retrieves the active window's active size minus the borders. Return: [Number], [Number] The width and height of the window's active bounds. --]] function Slab.GetWindowActiveSize() return Window.GetBorderlessSize() end --[[ IsWindowAppearing Is the current window appearing this frame. This will return true if BeginWindow has not been called for a window over 2 or more frames. Return: [Boolean] True if the window is appearing this frame. False otherwise. --]] function Slab.IsWindowAppearing() return Window.IsAppearing() end --[[ PushID Pushes a custom ID onto a stack. This allows developers to differentiate between similar controls such as text controls. ID: [String] The custom ID to add. Return: None. --]] function Slab.PushID(ID) assert(type(ID) == 'string', "'ID' parameter must be a string value.") Window.PushID(ID) end --[[ PopID Pops the last custom ID from the stack. Return: None. --]] function Slab.PopID() Window.PopID() end --[[ BeginMainMenuBar This function begins the process for setting up the main menu bar. This should be called outside of any BeginWindow/EndWindow calls. The user should only call EndMainMenuBar if this function returns true. Use BeginMenu/EndMenu calls to add menu items on the main menu bar. Example: if Slab.BeginMainMenuBar() then if Slab.BeginMenu("File") then if Slab.MenuItem("Quit") then love.event.quit() end Slab.EndMenu() end Slab.EndMainMenuBar() end Return: [Boolean] Returns true if the main menu bar process has started. --]] function Slab.BeginMainMenuBar() local X,Y = 0.0, 0.0 if Utility.IsMobile() then X, Y = love.window.getSafeArea() end Cursor.SetPosition(X, Y) return Slab.BeginMenuBar(true) end --[[ EndMainMenuBar This function should be called if BeginMainMenuBar returns true. Return: None. --]] function Slab.EndMainMenuBar() Slab.EndMenuBar() end --[[ BeginMenuBar This function begins the process of rendering a menu bar for a window. This should only be called within a BeginWindow/EndWindow context. IsMainMenuBar: [Boolean] Is this menu bar for the main viewport. Used internally. Should be ignored for all other calls. Return: [Boolean] Returns true if the menu bar process has started. --]] function Slab.BeginMenuBar(IsMainMenuBar) return MenuBar.Begin(IsMainMenuBar) end --[[ EndMenuBar This function should be called if BeginMenuBar returns true. Return: None. --]] function Slab.EndMenuBar() MenuBar.End() end --[[ BeginMenu Adds a menu item that when the user hovers over, opens up an additional context menu. When used within a menu bar, BeginMenu calls will be added to the bar. Within a context menu, the menu item will be added within the context menu with an additional arrow to notify the user more options are available. If this function returns true, the user must call EndMenu. Label: [String] The label to display for this menu. Options: [Table] List of options that control how this menu behaves. Enabled: [Boolean] Determines if this menu is enabled. This value is true by default. Disabled items are displayed but cannot be interacted with. Return: [Boolean] Returns true if the menu item is being hovered. --]] function Slab.BeginMenu(Label, Options) return Menu.BeginMenu(Label, Options) end --[[ EndMenu Finishes up a BeginMenu. This function must be called if BeginMenu returns true. Return: None. --]] function Slab.EndMenu() Menu.EndMenu() end --[[ BeginContextMenuItem Opens up a context menu based on if the user right clicks on the last item. This function should be placed immediately after an item call to open up a context menu for that specific item. If this function returns true, EndContextMenu must be called. Example: if Slab.Button("Button!") then -- Perform logic here when button is clicked end -- This will only return true if the previous button is hot and the user right-clicks. if Slab.BeginContextMenuItem() then Slab.MenuItem("Button Item 1") Slab.MenuItem("Button Item 2") Slab.EndContextMenu() end Button: [Number] The mouse button to use for opening up this context menu. Return: [Boolean] Returns true if the user right clicks on the previous item call. EndContextMenu must be called in order for this to function properly. --]] function Slab.BeginContextMenuItem(Button) return Menu.BeginContextMenu({IsItem = true, Button = Button}) end --[[ BeginContextMenuWindow Opens up a context menu based on if the user right clicks anywhere within the window. It is recommended to place this function at the end of a window's widget calls so that Slab can catch any BeginContextMenuItem calls before this call. If this function returns true, EndContextMenu must be called. Button: [Number] The mouse button to use for opening up this context menu. Return: [Boolean] Returns true if the user right clicks anywhere within the window. EndContextMenu must be called in order for this to function properly. --]] function Slab.BeginContextMenuWindow(Button) return Menu.BeginContextMenu({IsWindow = true, Button = Button}) end --[[ EndContextMenu Finishes up any BeginContextMenuItem/BeginContextMenuWindow if they return true. Return: None. --]] function Slab.EndContextMenu() Menu.EndContextMenu() end --[[ MenuItem Adds a menu item to a given context menu. Label: [String] The label to display to the user. Options: [Table] List of options that control how this menu behaves. Enabled: [Boolean] Determines if this menu is enabled. This value is true by default. Disabled items are displayed but cannot be interacted with. Hint: [String] Show an input hint to the right of the menu item Return: [Boolean] Returns true if the user clicks on this menu item. --]] function Slab.MenuItem(Label, Options) return Menu.MenuItem(Label, Options) end --[[ MenuItemChecked Adds a menu item to a given context menu. If IsChecked is true, then a check mark will be rendered next to the label. Example: local Checked = false if Slab.MenuItemChecked("Menu Item", Checked) Checked = not Checked end Label: [String] The label to display to the user. IsChecked: [Boolean] Determines if a check mark should be rendered next to the label. Options: [Table] List of options that control how this menu behaves. Enabled: [Boolean] Determines if this menu is enabled. This value is true by default. Disabled items are displayed but cannot be interacted with. Return: [Boolean] Returns true if the user clicks on this menu item. --]] function Slab.MenuItemChecked(Label, IsChecked, Options) return Menu.MenuItemChecked(Label, IsChecked, Options) end --[[ Separator This functions renders a separator line in the window. Option: [Table] List of options for how this separator will be drawn. IncludeBorders: [Boolean] Whether to extend the separator to include the window borders. This is false by default. H: [Number] The height of the separator. This doesn't change the line thickness, rather, specifies the cursor advancement in the Y direction. Thickness: [Number] The thickness of the line rendered. The default value is 1.0. Return: None. --]] function Slab.Separator(Options) Separator.Begin(Options) end --[[ Button Adds a button to the active window. Label: [String] The label to display on the button. Options: [Table] List of options for how this button will behave. Tooltip: [String] The tooltip to display when the user hovers over this button. Rounding: [Number] Amount of rounding to apply to the corners of the button. Invisible: [Boolean] Don't render the button, but keep the behavior. W: [Number] Override the width of the button. H: [Number] Override the height of the button. Disabled: [Boolean] If true, the button is not interactable by the user. Image: [Table] A table of options used to draw an image instead of a text label. Refer to the 'Image' documentation for a list of available options. Color: [Table]: The background color of the button when idle. The default value is the ButtonColor property in the Style's table. HoverColor: [Table]: The background color of the button when a mouse is hovering the control. The default value is the ButtonHoveredColor property in the Style's table. PressColor: [Table]: The background color of the button when the button is pressed but not released. The default value is the ButtonPressedColor property in the Style's table. PadX: [Number] Amount of additional horizontal space the background will expand to from the center. The default value is 20. PadY: [Number] Amount of additional vertical space the background will expand to from the center. The default value is 5. VLines: [Number] Number of lines in a multiline button text. The default value is 1. Return: [Boolean] Returns true if the user clicks on this button. --]] function Slab.Button(Label, Options) return Button.Begin(Label, Options) end --[[ RadioButton Adds a radio button entry to the active window. The grouping of radio buttons is determined by the user. An Index can be applied to the given radio button and a SelectedIndex can be passed in to determine if this specific radio button is the selected one. Label: [String] The label to display next to the button. Options: [Table] List of options for how this radio button will behave. Index: [Number] The index of this radio button. Will be 0 by default and not selectable. Assign an index to group the button. SelectedIndex: [Number] The index of the radio button that is selected. If this equals the Index field, then this radio button will be rendered as selected. Tooltip: [String] The tooltip to display when the user hovers over the button or label. Return: [Boolean] Returns true if the user clicks on this button. --]] function Slab.RadioButton(Label, Options) return Button.BeginRadio(Label, Options) end --[[ Text Adds text to the active window. Label: [String] The string to be displayed in the window. Options: [Table] List of options for how this text is displayed. Color: [Table] The color to render the text. Pad: [Number] How far to pad the text from the left side of the current cursor position. PadH: [Number] How far to pad the text vertically, will render centered in this region IsSelectable: [Boolean] Whether this text is selectable using the text's Y position and the window X and width as the hot zone. IsSelectableTextOnly: [Boolean] Will use the text width instead of the window width to determine the hot zone. Will set IsSelectable to true if that option is missing. IsSelected: [Boolean] Forces the hover background to be rendered. SelectOnHover: [Boolean] Returns true if the user is hovering over the hot zone of this text. HoverColor: [Table] The color to render the background if the IsSelected option is true. URL: [String] A URL address to open when this text control is clicked. Return: [Boolean] Returns true if SelectOnHover option is set to true. False otherwise. --]] function Slab.Text(Label, Options) return Text.Begin(Label, Options) end --[[ TextSelectable This function is a shortcut for SlabText with the IsSelectable option set to true. Label: [String] The string to be displayed in the window. Options: [Table] List of options for how this text is displayed. See Slab.Text for all options. Return: [Boolean] Returns true if user clicks on this text. False otherwise. --]] function Slab.TextSelectable(Label, Options) Options = Options == nil and {} or Options Options.IsSelectable = true return Slab.Text(Label, Options) end --[[ Textf Adds formatted text to the active window. This text will wrap to fit within the contents of either the window or a user specified width. Label: [String] The text to be rendered. Options: [Table] List of options for how this text is displayed. Color: [Table] The color to render the text. W: [Number] The width to restrict the text to. If this option is not specified, then the window width is used. Align: [String] The alignment to use for this text. For more information, refer to the love documentation at https://love2d.org/wiki/AlignMode. Below are the available options: center: Align text center. left: Align text left. right: Align text right. justify: Align text both left and right. Return: None. --]] function Slab.Textf(Label, Options) Text.BeginFormatted(Label, Options) end --[[ GetTextSize Retrieves the width and height of the given text. The result is based on the current font. Label: [String] The string to retrieve the size for. Return: [Number], [Number] The width and height of the given text. --]] function Slab.GetTextSize(Label) return Text.GetSize(Label) end --[[ GetTextWidth Retrieves the width of the given text. The result is based on the current font. Label: [String] The string to retrieve the width for. Return: [Number] The width of the given text. --]] function Slab.GetTextWidth(Label) local W, H = Slab.GetTextSize(Label) return W end --[[ GetTextHeight Retrieves the height of the current font. Return: [Number] The height of the current font. --]] function Slab.GetTextHeight() return Text.GetHeight() end --[[ CheckBox Renders a check box with a label. The check box when enabled will render an 'X'. Enabled: [Boolean] Will render an 'X' within the box if true. Will be an empty box otherwise. Label: [String] The label to display after the check box. Options: [Table] List of options for how this check box will behave. Tooltip: [String] Text to be displayed if the user hovers over the check box. Id: [String] An optional Id that can be supplied by the user. By default, the Id will be the label. Rounding: [Number] Amount of rounding to apply to the corners of the check box. Size: [Number] The uniform size of the box. The default value is 16. Disabled: [Boolean] Dictates whether this check box is enabled for interaction. Return: [Boolean] Returns true if the user clicks within the check box. --]] function Slab.CheckBox(Enabled, Label, Options) return CheckBox.Begin(Enabled, Label, Options) end --[[ Input This function will render an input box for a user to input text in. This widget behaves like input boxes found in other applications. This function will only return true if it has focus and user has either input text or pressed the return key. Example: local Text = "Hello World" if Slab.Input('Example', {Text = Text}) then Text = Slab.GetInputText() end Id: [String] A string that uniquely identifies this Input within the context of the window. Options: [Table] List of options for how this Input will behave. Tooltip: [String] Text to be displayed if the user hovers over the Input box. ReturnOnText: [Boolean] Will cause this function to return true whenever the user has input a new character into the Input box. This is true by default. Text: [String] The text to be supplied to the input box. It is recommended to use this option when ReturnOnText is true. TextColor: [Table] The color to use for the text. The default color is the color used for text, but there is also a default multiline text color defined in the Style. BgColor: [Table] The background color for the input box. SelectColor: [Table] The color used when the user is selecting text within the input box. SelectOnFocus: [Boolean] When this input box is focused by the user, the text contents within the input will be selected. This is true by default. NumbersOnly: [Boolean] When true, only numeric characters and the '.' character are allowed to be input into the input box. If no text is input, the input box will display '0'. W: [Number] The width of the input box. By default, will be 150.0 H: [Number] The height of the input box. By default, will be the height of the current font. ReadOnly: [Boolean] Whether this input field can be editable or not. Align: [String] Aligns the text within the input box. Options are: left: Aligns the text to the left. This will be set when this Input is focused. center: Aligns the text in the center. This is the default for when the text is not focused. Rounding: [Number] Amount of rounding to apply to the corners of the input box. MinNumber: [Number] The minimum value that can be entered into this input box. Only valid when NumbersOnly is true. MaxNumber: [Number] The maximum value that can be entered into this input box. Only valid when NumbersOnly is true. MultiLine: [Boolean] Determines whether this input control should support multiple lines. If this is true, then the SelectOnFocus flag will be false. The given text will also be sanitized to remove controls characters such as '\r'. Also, the text will be left aligned. MultiLineW: [Number] The width for which the lines of text should be wrapped at. Highlight: [Table] A list of key-values that define what words to highlight what color. Strings should be used for the word to highlight and the value should be a table defining the color. Step: [Number] The step amount for numeric controls when the user click and drags. The default value is 1.0. NoDrag: [Boolean] Determines whether this numberic control allows the user to click and drag to alter the value. UseSlider: [Boolean] If enabled, displays a slider inside the input control. This will only be drawn if the NumbersOnly option is set to true. The position of the slider inside the control determines the value based on the MinNumber and MaxNumber option. IsPassword: [Boolean] If enabled, mask the text with another character. Default is false. PasswordChar: [Char/String] Sets the character or string to use along with IsPassword flag. Default is "*" Return: [Boolean] Returns true if the user has pressed the return key while focused on this input box. If ReturnOnText is set to true, then this function will return true whenever the user has input any character into the input box. --]] function Slab.Input(Id, Options) return Input.Begin(Id, Options) end --[[ InputNumberDrag This is a wrapper function for calling the Input function which sets the proper options to set up the input box for displaying and editing numbers. The user will be able to click and drag the control to alter the value. Double-clicking inside this control will allow for manually editing the value. Id: [String] A string that uniquely identifies this Input within the context of the window. Value: [Number] The value to display in the control. Min: [Number] The minimum value that can be set for this number control. If nil, then this value will be set to -math.huge. Max: [Number] The maximum value that can be set for this number control. If nil, then this value will be set to math.huge. Step: [Number] The amount to increase value when mouse delta reaches threshold. Options: [Table] List of options for how this input control is displayed. See Slab.Input for all options. Return: [Boolean] Returns true whenever this valued is modified. --]] function Slab.InputNumberDrag(Id, Value, Min, Max, Step, Options) Options = Options == nil and {} or Options Options.Text = tostring(Value) Options.MinNumber = Min Options.MaxNumber = Max Options.Step = Step Options.NumbersOnly = true Options.UseSlider = false Options.NoDrag = false return Slab.Input(Id, Options) end --[[ InputNumberSlider This is a wrapper function for calling the Input function which sets the proper options to set up the input box for displaying and editing numbers. This will also force the control to display a slider, which determines what the value stored is based on the Min and Max options. Double-clicking inside this control will allow for manually editing the value. Id: [String] A string that uniquely identifies this Input within the context of the window. Value: [Number] The value to display in the control. Min: [Number] The minimum value that can be set for this number control. If nil, then this value will be set to -math.huge. Max: [Number] The maximum value that can be set for this number control. If nil, then this value will be set to math.huge. Options: [Table] List of options for how this input control is displayed. See Slab.Input for all options. Precision: [Number] An integer in the range [0..5]. This will set the size of the fractional component. NeedDrag: [Boolean] This will determine if slider needs to be dragged before changing value, otherwise just clicking in the slider will adjust the value into the clicked value. Default is true. Return: [Boolean] Returns true whenever this valued is modified. --]] function Slab.InputNumberSlider(Id, Value, Min, Max, Options) Options = Options == nil and {} or Options Options.Text = tostring(Value) Options.MinNumber = Min Options.MaxNumber = Max Options.NumbersOnly = true Options.UseSlider = true return Slab.Input(Id, Options) end --[[ GetInputText Retrieves the text entered into the focused input box. Refer to the documentation for Slab.Input for an example on how to use this function. Return: [String] Returns the text entered into the focused input box. --]] function Slab.GetInputText() return Input.GetText() end --[[ GetInputNumber Retrieves the text entered into the focused input box and attempts to conver the text into a number. Will always return a valid number. Return: [Number] Returns the text entered into the focused input box as a number. --]] function Slab.GetInputNumber() local Result = tonumber(Input.GetText()) if Result == nil then Result = 0 end return Result end --[[ GetInputCursorPos Retrieves the position of the input cursor for the focused input control. There are three values that are returned. The first one is the absolute position of the cursor with regards to the text for the control. The second is the column position of the cursor on the current line. The final value is the line number. The column will match the absolute position if the input control is not multi line. Return: [Number], [Number], [Number] The absolute position of the cursor, the column position of the cursor on the current line, and the line number of the cursor. These values will all be zero if no input control is focused. --]] function Slab.GetInputCursorPos() return Input.GetCursorPos() end --[[ IsInputFocused Returns whether the input control with the given Id is focused or not. Id: [String] The Id of the input control to check. Return: [Boolean] True if the input control with the given Id is focused. False otherwise. --]] function Slab.IsInputFocused(Id) return Input.IsFocused(Id) end --[[ IsAnyInputFocused Returns whether any input control is focused or not. Return: [Boolean] True if there is an input control focused. False otherwise. --]] function Slab.IsAnyInputFocused() return Input.IsAnyFocused() end --[[ SetInputFocus Sets the focus of the input control to the control with the given Id. The focus is set at the beginning of the next frame to avoid any input events from the current frame. Id: [String] The Id of the input control to focus. --]] function Slab.SetInputFocus(Id) Input.SetFocused(Id) end --[[ SetInputCursorPos Sets the absolute text position in bytes of the focused input control. This value is applied on the next frame. This function can be combined with the SetInputFocus function to modify the cursor positioning of the desired input control. Note that the input control supports UTF8 characters so if the desired position is not a valid character, the position will be altered to find the next closest valid character. Pos: [Number] The absolute position in bytes of the text of the focused input control. --]] function Slab.SetInputCursorPos(Pos) Input.SetCursorPos(Pos) end --[[ SetInputCursorPosLine Sets the column and line number of the focused input control. These values are applied on the next frame. This function behaves the same as SetInputCursorPos, but allows for setting the cursor by column and line. Column: [Number] The text position in bytes of the current line. Line: [Number] The line number to set. --]] function Slab.SetInputCursorPosLine(Column, Line) Input.SetCursorPosLine(Column, Line) end --[[ BeginTree This function will render a tree item with an optional label. The tree can be expanded or collapsed based on whether the user clicked on the tree item. This function can also be nested to create a hierarchy of tree items. This function will return false when collapsed and true when expanded. If this function returns true, Slab.EndTree must be called in order for this tree item to behave properly. The hot zone of this tree item will be the height of the label and the width of the window by default. Id: [String/Table] A string or table uniquely identifying this tree item within the context of the window. If the given Id is a table, then the internal Tree entry for this table will be removed once the table has been garbage collected. Options: [Table] List of options for how this tree item will behave. Label: [String] The text to be rendered for this tree item. Tooltip: [String] The text to be rendered when the user hovers over this tree item. IsLeaf: [Boolean] If this is true, this tree item will not be expandable/collapsable. OpenWithHighlight: [Boolean] If this is true, the tree will be expanded/collapsed when the user hovers over the hot zone of this tree item. If this is false, the user must click the expand/collapse icon to interact with this tree item. Icon: [Table] List of options to use for drawing the icon. Refer to the 'Image' documentation for more information. IsSelected: [Boolean] If true, will render a highlight rectangle around the tree item. IsOpen: [Boolean] Will force the tree item to be expanded. NoSavedSettings: [Boolean] Flag to disable saving this tree's settings to the state INI file. Return: [Boolean] Returns true if this tree item is expanded. Slab.EndTree must be called if this returns true. --]] function Slab.BeginTree(Id, Options) return Tree.Begin(Id, Options) end --[[ EndTree Finishes up any BeginTree calls if those functions return true. Return: None. --]] function Slab.EndTree() Tree.End() end --[[ BeginComboBox This function renders a non-editable input field with a drop down arrow. When the user clicks this option, a window is created and the user can supply their own Slab.TextSelectable calls to add possible items to select from. This function will return true if the combo box is opened. Slab.EndComboBox must be called if this function returns true. Example: local Options = {"Apple", "Banana", "Orange", "Pear", "Lemon"} local Options_Selected = "" if Slab.BeginComboBox('Fruits', {Selected = Options_Selected}) then for K, V in pairs(Options) do if Slab.TextSelectable(V) then Options_Selected = V end end Slab.EndComboBox() end Id: [String] A string that uniquely identifies this combo box within the context of the active window. Options: [Table] List of options that control how this combo box behaves. Tooltip: [String] Text that is rendered when the user hovers over this combo box. Selected: [String] Text that is displayed in the non-editable input box for this combo box. W: [Number] The width of the combo box. The default value is 150.0. Rounding: [Number] Amount of rounding to apply to the corners of the combo box. Return: [Boolean] This function will return true if the combo box is open. --]] function Slab.BeginComboBox(Id, Options) return ComboBox.Begin(Id, Options) end --[[ EndComboBox Finishes up any BeginComboBox calls if those functions return true. Return: None. --]] function Slab.EndComboBox() ComboBox.End() end --[[ Image Draws an image at the current cursor position. The Id uniquely identifies this image to manage behaviors with this image. An image can be supplied through the options or a path can be specified which Slab will manage the loading and storing of the image reference. Id: [String] A string uniquely identifying this image within the context of the current window. Options: [Table] List of options controlling how the image should be drawn. Image: [Object] A user supplied image. This must be a valid Love image or the call will assert. Path: [String] If the Image option is nil, then a path must be specified. Slab will load and manage the image resource. Rotation: [Number] The rotation value to apply when this image is drawn. Scale: [Number] The scale value to apply to both the X and Y axis. ScaleX: [Number] The scale value to apply to the X axis. ScaleY: [Number] The scale value to apply to the Y axis. Color: [Table] The color to use when rendering this image. SubX: [Number] The X-coordinate used inside the given image. SubY: [Number] The Y-coordinate used inside the given image. SubW: [Number] The width used inside the given image. SubH: [Number] The height used insided the given image. WrapX: [String] The horizontal wrapping mode for this image. The available options are 'clamp', 'repeat', 'mirroredrepeat', and 'clampzero'. For more information refer to the Love2D documentation on wrap modes at https://love2d.org/wiki/WrapMode. WrapY: [String] The vertical wrapping mode for this image. The available options are 'clamp', 'repeat', 'mirroredrepeat', and 'clampzero'. For more information refer to the Love2D documentation on wrap modes at https://love2d.org/wiki/WrapMode. UseOutline: [Boolean] If set to true, a rectangle will be drawn around the given image. If 'SubW' or 'SubH' are specified, these values will be used instead of the image's dimensions. OutlineColor: [Table] The color used to draw the outline. Default color is black. OutlineW: [Number] The width used for the outline. Default value is 1. W: [Number] The width the image should be resized to. H: [Number] The height the image should be resized to. Return: None. --]] function Slab.Image(Id, Options) Image.Begin(Id, Options) end --[[ SameLine This forces the cursor to move back up to the same line as the previous widget. By default, all Slab widgets will advance the cursor to the next line based on the height of the current line. By using this call with other widget calls, the user will be able to set up multiple widgets on the same line to control how a window may look. Options: [Table] List of options that controls how the cursor should handle the same line. Pad: [Number] Extra padding to apply in the X direction. CenterY: [Boolean] Controls whether the cursor should be centered in the Y direction on the line. By default the line will use the NewLineSize, which is the height of the current font to center the cursor. Return: None. --]] function Slab.SameLine(Options) LayoutManager.SameLine(Options) end --[[ NewLine This forces the cursor to advance to the next line based on the height of the current font. Count: [Number] Specify how many new lines to insert, defaults to 1 Return: None. --]] function Slab.NewLine(Count) Count = Count or 1 for i = 1, Count do LayoutManager.NewLine() end end --[[ SetCursorPos Sets the cursor position. The default behavior is to set the cursor position relative to the current window. The absolute position can be set if the 'Absolute' option is set. Controls will only be drawn within a window. If the cursor is set outside of the current window context, the control will not be displayed. X: [Number] The X coordinate to place the cursor. If nil, then the X coordinate is not modified. Y: [Number] The Y coordinate to place the cursor. If nil, then the Y coordinate is not modified. Options: [Table] List of options that control how the cursor position should be set. Absolute: [Boolean] If true, will place the cursor using absolute coordinates. Return: None. --]] function Slab.SetCursorPos(X, Y, Options) Options = Options == nil and {} or Options Options.Absolute = Options.Absolute == nil and false or Options.Absolute if Options.Absolute then X = X == nil and Cursor.GetX() or X Y = Y == nil and Cursor.GetY() or Y Cursor.SetPosition(X, Y) else X = X == nil and Cursor.GetX() - Cursor.GetAnchorX() or X Y = Y == nil and Cursor.GetY() - Cursor.GetAnchorY() or Y Cursor.SetRelativePosition(X, Y) end end --[[ GetCursorPos Gets the cursor position. The default behavior is to get the cursor position relative to the current window. The absolute position can be retrieved if the 'Absolute' option is set. Options: [Table] List of options that control how the cursor position should be retrieved. Absolute: [Boolean] If true, will return the cursor position in absolute coordinates. Return: [Number], [Number] The X and Y coordinates of the cursor. --]] function Slab.GetCursorPos(Options) Options = Options == nil and {} or Options Options.Absolute = Options.Absolute == nil and false or Options.Absolute local X, Y = Cursor.GetPosition() if not Options.Absolute then X = X - Cursor.GetAnchorX() Y = Y - Cursor.GetAnchorY() end return X, Y end --[[ Indent Advances the anchored X position of the cursor. All subsequent lines will begin at the new cursor position. This function has no effect when columns are present. Width: [Number] How far in pixels to advance the cursor. If nil, then the default value identified by the 'Indent' property in the current style is used. Return: None. --]] function Slab.Indent(Width) Width = Width == nil and Style.Indent or Width Cursor.Indent(Width) end --[[ Unindent Retreats the anchored X position of the cursor. All subsequent lines will begin at the new cursor position. This function has no effect when columns are present. Width: [Number] How far in pixels to retreat the cursor. If nil, then the default value identified by the 'Indent' property in the current style is used. Return: None. --]] function Slab.Unindent(Width) Width = Width == nil and Style.Indent or Width Cursor.Unindent(Width) end --[[ Properties Iterates through the table's key-value pairs and adds them to the active window. This currently only does a shallow loop and will not iterate through nested tables. TODO: Iterate through nested tables. Table: [Table] The list of properties to build widgets for. Options: [Table] List of options that can applied to a specific property. The key should match an entry in the 'Table' argument and will apply any additional options to the property control. Fallback: [Table] List of options that can be applied to any property if an entry was not found in the 'Options' argument. Return: None. --]] function Slab.Properties(Table, Options, Fallback) Options = Options or {} Fallback = Fallback or {} if Table ~= nil then for I, T in ipairs(Table) do local V = T.Value local Type = type(V) local ID = T.ID local ItemOptions = Options[ID] or Fallback if Type == "boolean" then if Slab.CheckBox(V, ID, ItemOptions) then T.Value = not T.Value end elseif Type == "number" then Slab.Text(ID .. ": ") Slab.SameLine() ItemOptions.Text = V ItemOptions.NumbersOnly = true ItemOptions.ReturnOnText = false ItemOptions.UseSlider = ItemOptions.MinNumber and ItemOptions.MaxNumber if Slab.Input(ID, ItemOptions) then T.Value = Slab.GetInputNumber() end elseif Type == "string" then Slab.Text(ID .. ": ") Slab.SameLine() ItemOptions.Text = V ItemOptions.NumbersOnly = false ItemOptions.ReturnOnText = false if Slab.Input(ID, ItemOptions) then T.Value = Slab.GetInputText() end end end end end --[[ BeginListBox Begins the process of creating a list box. If this function is called, EndListBox must be called after all items have been added. Id: [String] A string uniquely identifying this list box within the context of the current window. Options: [Table] List of options controlling the behavior of the list box. W: [Number] The width of the list box. If nil, a default value of 150 is used. H: [Number] The height of the list box. If nil, a default value of 150 is used. Clear: [Boolean] Clears out the items in the list. It is recommended to only call this if the list items has changed and should not be set to true on every frame. Rounding: [Number] Amount of rounding to apply to the corners of the list box. StretchW: [Boolean] Stretch the list box to fill the remaining width of the window. StretchH: [Boolean] Stretch the list box to fill the remaining height of the window. Return: None. --]] function Slab.BeginListBox(Id, Options) ListBox.Begin(Id, Options) end --[[ EndListBox Ends the list box container. Will close off the region and properly adjust the cursor. Return: None. --]] function Slab.EndListBox() ListBox.End() end --[[ BeginListBoxItem Adds an item to the current list box with the given Id. The user can then draw controls however they see fit to display a single item. This allows the user to draw list items such as a texture with a name or just a text to represent the item. If this is called, EndListBoxItem must be called to complete the item. Id: [String] A string uniquely identifying this item within the context of the current list box. Options: [Table] List of options that control the behavior of the active list item. Selected: [Boolean] If true, will draw the item with a selection background. Return: None. --]] function Slab.BeginListBoxItem(Id, Options) ListBox.BeginItem(Id, Options) end --[[ IsListBoxItemClicked Checks to see if a hot list item is clicked. This should only be called within a BeginListBoxLitem/EndListBoxItem block. Button: [Number] The button to check for the click of the item. IsDoubleClick: [Boolean] Check for double-click instead of single click. Return: [Boolean] Returns true if the active item is hovered with mouse and the requested mouse button is clicked. --]] function Slab.IsListBoxItemClicked(Button, IsDoubleClick) return ListBox.IsItemClicked(Button, IsDoubleClick) end --[[ EndListBoxItem Ends the current item and commits the bounds of the item to the list. Return: None. --]] function Slab.EndListBoxItem() ListBox.EndItem() end --[[ OpenDialog Opens the dialog box with the given Id. If the dialog box was opened, then it is pushed onto the stack. Calls to the BeginDialog with this same Id will return true if opened. Id: [String] A string uniquely identifying this dialog box. Return: None. --]] function Slab.OpenDialog(Id) Dialog.Open(Id) end --[[ BeginDialog Begins the dialog window with the given Id if it is open. If this function returns true, then EndDialog must be called. Dialog boxes are windows which are centered in the center of the viewport. The dialog box cannot be moved and will capture all input from all other windows. Id: [String] A string uniquely identifying this dialog box. Options: [Table] List of options that control how this dialog box behaves. These are the same parameters found for BeginWindow, with some caveats. Certain options are overridden by the Dialog system. They are: X, Y, Layer, AllowFocus, AllowMove, and AutoSizeWindow. Return: [Boolean] Returns true if the dialog with the given Id is open. --]] function Slab.BeginDialog(Id, Options) return Dialog.Begin(Id, Options) end --[[ EndDialog Ends the dialog window if a call to BeginDialog returns true. Return: None. --]] function Slab.EndDialog() Dialog.End() end --[[ CloseDialog Closes the currently active dialog box. Return: None. --]] function Slab.CloseDialog() Dialog.Close() end --[[ MessageBox Opens a message box to be displayed to the user with a title and a message. Buttons can be specified through the options table which when clicked, the string of the button is returned. This function should be called every frame when a message box wants to be displayed. Title: [String] The title to display for the message box. Message: [String] The message to be displayed. The text is aligned in the center. Multi-line strings are supported. Options: [Table] List of options to control the behavior of the message box. Buttons: [Table] List of buttons to display with the message box. The order of the buttons are displayed from right to left. Return: [String] The name of the button that was clicked. If none was clicked, an emtpy string is returned. --]] function Slab.MessageBox(Title, Message, Options) return Dialog.MessageBox(Title, Message, Options) end --[[ FileDialog Opens up a dialog box that displays a file explorer for opening or saving files or directories. This function does not create any file handles, it just returns the list of files selected by the user. Options: [Table] List of options that control the behavior of the file dialog. AllowMultiSelect: [Boolean] Allows the user to select multiple items in the file dialog. Directory: [String] The starting directory when the file dialog is open. If none is specified, the dialog will start at love.filesystem.getSourceBaseDirectory and the dialog will remember the last directory navigated to by the user between calls to this function. Type: [String] The type of file dialog to use. The options are: openfile: This is the default method. The user will have access to both directories and files. However, only file selections are returned. opendirectory: This type is used to filter the file dialog for directories only. No files will appear in the list. savefile: This type is used to select a name of a file to save. The user will be prompted if they wish to overwrite an existing file. Filters: [Table] A list of filters the user can select from when browsing files. The table can contain tables or strings. Table: If a table is used for a filter, it should contain two elements. The first element is the filter while the second element is the description of the filter e.g. {"*.lua", "Lua Files"} String: If a raw string is used, then it should just be the filter. It is recommended to use the table option since a description can be given for each filter. IncludeParent: [Boolean] This option will include the parent '..' directory item in the file/dialog list. This option is true by default. Return: [Table] Returns items for how the user interacted with this file dialog. Button: [String] The button the user clicked. Will either be OK or Cancel. Files: [Table] An array of selected file items the user selected when OK is pressed. Will be empty otherwise. --]] function Slab.FileDialog(Options) return Dialog.FileDialog(Options) end --[[ ColorPicker Displays a window to allow the user to pick a hue and saturation value of a color. This should be called every frame and the result should be handled to stop displaying the color picker and store the resulting color. Options: [Table] List of options that control the behavior of the color picker. Color: [Table] The color to modify. This should be in the format of 0-1 for each color component (RGBA). Return: [Table] Returns the button and color the user has selected. Button: [Number] The button the user clicked. 1 - OK. 0 - No Interaction. -1 - Cancel. Color: [Table] The new color the user has chosen. This will always be returned. --]] function Slab.ColorPicker(Options) return ColorPicker.Begin(Options) end --[[ IsMouseDown Determines if a given mouse button is down. Button: [Number] The button to check for. The valid numbers are: 1 - Left, 2 - Right, 3 - Middle. Return: [Boolean] True if the given button is down. False otherwise. --]] function Slab.IsMouseDown(Button) return Mouse.IsDown(Button and Button or 1) end --[[ IsMouseClicked Determines if a given mouse button changes state from up to down this frame. Button: [Number] The button to check for. The valid numbers are: 1 - Left, 2 - Right, 3 - Middle. Return: [Boolean] True if the given button changes state from up to down. False otherwise. --]] function Slab.IsMouseClicked(Button) return Mouse.IsClicked(Button and Button or 1) end --[[ IsMouseReleased Determines if a given mouse button changes state from down to up this frame. Button: [Number] The button to check for. The valid numbers are: 1 - Left, 2 - Right, 3 - Middle. Return: [Boolean] True if the given button changes state from down to up. False otherwise. --]] function Slab.IsMouseReleased(Button) return Mouse.IsReleased(Button and Button or 1) end --[[ IsMouseDoubleClicked Determines if a given mouse button has been clicked twice within a given time frame. Button: [Number] The button to check for. The valid numbers are: 1 - Left, 2 - Right, 3 - Middle. Return: [Boolean] True if the given button was double clicked. False otherwise. --]] function Slab.IsMouseDoubleClicked(Button) return Mouse.IsDoubleClicked(Button and Button or 1) end --[[ IsMouseDragging Determines if a given mouse button is down and there has been movement. Button: [Number] The button to check for. The valid numbers are: 1 - Left, 2 - Right, 3 - Middle. Return: [Boolean] True if the button is held down and is moving. False otherwise. --]] function Slab.IsMouseDragging(Button) return Mouse.IsDragging(Button and Button or 1) end --[[ GetMousePosition Retrieves the current mouse position in the viewport. Return: [Number], [Number] The X and Y coordinates of the mouse position. --]] function Slab.GetMousePosition() return Mouse.Position() end --[[ GetMousePositionWindow Retrieves the current mouse position within the current window. This position will include any transformations added to the window such as scrolling. Return: [Number], [Number] The X and Y coordinates of the mouse position within the window. --]] function Slab.GetMousePositionWindow() return Window.GetMousePosition() end --[[ GetMouseDelta Retrieves the change in mouse coordinates from the last frame. Return: [Number], [Number] The X and Y coordinates of the delta from the last frame. --]] function Slab.GetMouseDelta() return Mouse.GetDelta() end --[[ SetCustomMouseCursor Overrides a system mouse cursor of the given type to render a custom image instead. Type: [String] The system cursor type to replace. This can be one of the following values: 'arrow', 'sizewe', 'sizens', 'sizenesw', 'sizenwse', 'ibeam', 'hand'. Image: [Table] An 'Image' object created from love.graphics.newImage. If this is nil, then an empty image is created and is drawn when the system cursor is activated. Quad: [Table] A 'Quad' object created from love.graphics.newQuad. This allows support for setting UVs of an image to render. --]] function Slab.SetCustomMouseCursor(Type, Image, Quad) Mouse.SetCustomCursor(Type, Image, Quad) end --[[ ClearCustomMouseCursor Removes any override of a system mouse cursor with the given type and defaults to the OS specific mouse cursor. Type: [String] The system cursor type to remove. This can be one of the following values: 'arrow', 'sizewe', 'sizens', 'sizenesw', 'sizenwse', 'ibeam', 'hand'. --]] function Slab.ClearCustomMouseCursor(Type) Mouse.ClearCustomCursor(Type) end --[[ IsControlHovered Checks to see if the last control added to the window is hovered by the mouse. Return: [Boolean] True if the last control is hovered, false otherwise. --]] function Slab.IsControlHovered() -- Prevent hovered checks on mobile if user is not dragging a touch. if Utility.IsMobile() and not Slab.IsMouseDown() then return false end local Result = Window.IsItemHot() if not Result and not Window.IsObstructedAtMouse() then local X, Y = Slab.GetMousePositionWindow() Result = Cursor.IsInItemBounds(X, Y) end return Result end --[[ IsControlClicked Checks to see if the previous control is hovered and clicked. Button: [Number] The button to check for. The valid numbers are: 1 - Left, 2 - Right, 3 - Middle. Return: [Boolean] True if the previous control is hovered and clicked. False otherwise. --]] function Slab.IsControlClicked(Button) return Slab.IsControlHovered() and Slab.IsMouseClicked(Button) end --[[ GetControlSize Retrieves the last declared control's size. Return: [Number], [Number] The width and height of the last control declared. --]] function Slab.GetControlSize() local X, Y, W, H = Cursor.GetItemBounds() return W, H end --[[ IsVoidHovered Checks to see if any non-Slab area of the viewport is hovered. Return: [Boolean] True if any non-Slab area of the viewport is hovered. False otherwise. --]] function Slab.IsVoidHovered() -- Prevent hovered checks on mobile if user is not dragging a touch. if Utility.IsMobile() and not Slab.IsMouseDown() then return false end return Region.GetHotInstanceId() == '' and not Region.IsScrolling() end --[[ IsVoidClicked Checks to see if any non-Slab area of the viewport is clicked. Button: [Number] The button to check for. The valid numbers are: 1 - Left, 2 - Right, 3 - Middle. Return: [Boolean] True if any non-Slab area of the viewport is clicked. False otherwise. --]] function Slab.IsVoidClicked(Button) return Slab.IsMouseClicked(Button) and Slab.IsVoidHovered() end --[[ IsKeyDown Checks to see if a specific key is held down. The key should be one of the love defined Scancode which the list can be found at https://love2d.org/wiki/Scancode. Key: [String] A love defined key scancode. Return: [Boolean] True if the key is held down. False otherwise. --]] function Slab.IsKeyDown(Key) return Keyboard.IsDown(Key) end --[[ IsKeyPressed Checks to see if a specific key state went from up to down this frame. The key should be one of the love defined Scancode which the list can be found at https://love2d.org/wiki/Scancode. Key: [String] A love defined scancode. Return: [Boolean] True if the key state went from up to down this frame. False otherwise. --]] function Slab.IsKeyPressed(Key) return Keyboard.IsPressed(Key) end --[[ IsKeyPressed Checks to see if a specific key state went from down to up this frame. The key should be one of the love defined Scancode which the list can be found at https://love2d.org/wiki/Scancode. Key: [String] A love defined scancode. Return: [Boolean] True if the key state went from down to up this frame. False otherwise. --]] function Slab.IsKeyReleased(Key) return Keyboard.IsReleased(Key) end --[[ Rectangle Draws a rectangle at the current cursor position for the active window. Options: [Table] List of options that control how this rectangle is displayed. Mode: [String] Whether this rectangle should be filled or outlined. The default value is 'fill'. W: [Number] The width of the rectangle. H: [Number] The height of the rectangle. Color: [Table] The color to use for this rectangle. Rounding: [Number] or [Table] [Number] Amount of rounding to apply to all corners. [Table] Define the rounding for each corner. The order goes top left, top right, bottom right, and bottom left. Outline: [Boolean] If the Mode option is 'fill', this option will allow an outline to be drawn. OutlineColor: [Table] The color to use for the outline if requested. Segments: [Number] Number of points to add for each corner if rounding is requested. Return: None. --]] function Slab.Rectangle(Options) Shape.Rectangle(Options) end --[[ Circle Draws a circle at the current cursor position plus the radius for the active window. Options: [Table] List of options that control how this circle is displayed. Mode: [String] Whether this circle should be filled or outlined. The default value is 'fill'. Radius: [Number] The size of the circle. Color: [Table] The color to use for the circle. Segments: [Number] The number of segments used for drawing the circle. Return: None. --]] function Slab.Circle(Options) Shape.Circle(Options) end --[[ Triangle Draws a triangle at the current cursor position plus the radius for the active window. Option: [Table] List of options that control how this triangle is displayed. Mode: [String] Whether this triangle should be filled or outlined. The default value is 'fill'. Radius: [Number] The distance from the center of the triangle. Rotation: [Number] The rotation of the triangle in degrees. Color: [Table] The color to use for the triangle. Return: None. --]] function Slab.Triangle(Options) Shape.Triangle(Options) end --[[ Line Draws a line starting at the current cursor position and going to the defined points in this function. X2: [Number] The X coordinate for the destination. Y2: [Number] The Y coordinate for the destination. Option: [Table] List of options that control how this line is displayed. Width: [Number] How thick the line should be. Color: [Table] The color to use for the line. Return: None. --]] function Slab.Line(X2, Y2, Options) Shape.Line(X2, Y2, Options) end --[[ Curve Draws a bezier curve with the given points as control points. The points should be defined in local space. Slab will translate the curve to the current cursor position. There should two or more points defined for a proper curve. Points: [Table] List of points to define the control points of the curve. Options: [Table] List of options that control how this curve is displayed. Color: [Table] The color to use for this curve. Depth: [Number] The number of recursive subdivision steps to use when rendering the curve. If nil, the default LÖVE 2D value is used which is 5. Return: None. --]] function Slab.Curve(Points, Options) Shape.Curve(Points, Options) end --[[ GetCurveControlPointCount Returns the number of control points defined with the last call to Curve. Return: [Number] The number of control points defined for the previous curve. --]] function Slab.GetCurveControlPointCount() return Shape.GetCurveControlPointCount() end --[[ GetCurveControlPoint Returns the point for the given control point index. This point by default will be in local space defined by the points given in the Curve function. The translated position can be requested by setting the LocalSpace option to false. Index: [Number] The index of the control point to retrieve. Options: [Table] A list of options that control what is returned by this function. LocalSpace: [Boolean] Returns either the translated or untranslated control point. This is true by default. Return: [Number], [Number] The translated X, Y coordinates of the given control point. --]] function Slab.GetCurveControlPoint(Index, Options) return Shape.GetCurveControlPoint(Index, Options) end --[[ EvaluateCurve Returns the point at the given time. The time value should be between 0 and 1 inclusive. The point returned will be in local space. For the translated position, set the LocalSpace option to false. Time: [Number] The time on the curve between 0 and 1. Options: [Table] A list of options that control what is returned by this function. LocalSpace: [Boolean] Returnes either the translated or untranslated control point. This is true by default. Return: [Number], [Number] The X and Y coordinates at the given time on the curve. --]] function Slab.EvaluateCurve(Time, Options) return Shape.EvaluateCurve(Time, Options) end --[[ EvaluateCurveMouse Returns the point on the curve at the given X-coordinate of the mouse relative to the end points of the curve. Options: [Table] A list of options that control what is returned by this function. Refer to the documentation for EvaluateCurve for the list of options. Return: [Number], [Number] The X and Y coordinates at the given X mouse position on the curve. --]] function Slab.EvaluateCurveMouse(Options) local X1, Y1 = Slab.GetCurveControlPoint(1, {LocalSpace = false}) local X2, Y2 = Slab.GetCurveControlPoint(Slab.GetCurveControlPointCount(), {LocalSpace = false}) local Left = math.min(X1, X2) local W = math.abs(X2 - X1) local X, Y = Slab.GetMousePositionWindow() local Offset = math.max(X - Left, 0.0) Offset = math.min(Offset, W) return Slab.EvaluateCurve(Offset / W, Options) end --[[ Polygon Renders a polygon with the given points. The points should be defined in local space. Slab will translate the position to the current cursor position. Points: [Table] List of points that define this polygon. Options: [Table] List of options that control how this polygon is drawn. Color: [Table] The color to render this polygon. Mode: [String] Whether to use 'fill' or 'line' to draw this polygon. The default is 'fill'. Return: None. --]] function Slab.Polygon(Points, Options) Shape.Polygon(Points, Options) end --[[ BeginStat Starts the timer for the specific stat in the given category. Name: [String] The name of the stat to capture. Category: [String] The category this stat belongs to. Return: [Number] The handle identifying this stat capture. --]] function Slab.BeginStat(Name, Category) return Stats.Begin(Name, Category) end --[[ EndStat Ends the timer for the stat assigned to the given handle. Handle: [Number] The handle identifying a BeginStat call. Return: None. --]] function Slab.EndStat(Handle) Stats.End(Handle) end --[[ EnableStats Sets the enabled state of the stats system. The system is disabled by default. Enable: [Boolean] The new state of the states system. Return: None. --]] function Slab.EnableStats(Enable) Stats.SetEnabled(Enable) end --[[ IsStatsEnabled Query whether the stats system is enabled or disabled. Return: [Boolean] Returns whether the stats system is enabled or disabled. --]] function Slab.IsStatsEnabled() return Stats.IsEnabled() end --[[ FlushStats Resets the stats system to an empty state. Return: None. --]] function Slab.FlushStats() Stats.Flush() end --[[ GetStats Get the love.graphics.getStats of the Slab. Stats.SetEnabled(true) must be previously set to enable this. Must be called in love.draw (recommended at the end of draw) Return: Table. --]] function Slab.GetStats() return StatsData end --[[ CalculateStats Calculate the passed love.graphics.getStats table of love by subtracting the stats of Slab. Stats.SetEnabled(true) must be previously set to enable this. Must be called in love.draw (recommended at the end of draw) Return: Table. --]] function Slab.CalculateStats(LoveStats) for k, v in pairs(LoveStats) do if StatsData[k] then LoveStats[k] = v - StatsData[k] end end return LoveStats end --[[ BeginLayout Enables the layout manager and positions the controls between this call and EndLayout based on the given options. The anchor position for the layout is determined by the current cursor position on the Y axis. The horizontal position is not anchored. Layouts are stacked, so there can be layouts within parent layouts. Id: [String] The Id of this layout. Options: [Table] List of options that control how this layout behaves. AlignX: [String] Defines how the controls should be positioned horizontally in the window. The available options are 'left', 'center', or 'right'. The default option is 'left'. AlignY: [String] Defines how the controls should be positioned vertically in the window. The available options are 'top', 'center', or 'bottom'. The default option is 'top'. The top is determined by the current cursor position. AlignRowY: [String] Defines how the controls should be positioned vertically within a row. The available options are 'top', 'center', or 'bottom'. The default option is 'top'. Ignore: [Boolean] Should this layout ignore positioning of controls. This is useful if certain controls need custom positioning within a layout. ExpandW: [Boolean] If true, will expand all controls' width within the row to the size of the window. ExpandH: [Boolean] If true, will expand all controls' height within the row and the size of the window. AnchorX: [Boolean] Anchors the layout management at the current X cursor position. The size is calculated using this position. The default value for this is false. AnchorY: [Boolean] Anchors the layout management at the current Y cursor position. The size is calculated using this position. The default value for this is true. Columns: [Number] The number of columns to use for this layout. The default value is 1. Return: None. --]] function Slab.BeginLayout(Id, Options) LayoutManager.Begin(Id, Options) end --[[ EndLayout Ends the currently active layout. Each BeginLayout call must have a matching EndLayout. Failure to do so will result in an assertion. Return: None. --]] function Slab.EndLayout() LayoutManager.End() end --[[ SetLayoutColumn Sets the current active column. Index: [Number] The index of the column to be active. Return: None. --]] function Slab.SetLayoutColumn(Index) LayoutManager.SetColumn(Index) end --[[ GetLayoutSize Retrieves the size of the active layout. If there are columns, then the size of the column is returned. Return: [Number], [Number] The width and height of the active layout. 0 is returned if no layout is active. --]] function Slab.GetLayoutSize() return LayoutManager.GetActiveSize() end --[[ GetCurrentColumnIndex Retrieves the current index of the active column. Return: [Number] The current index of the active column of the active layout. 0 is returned if no layout or column is active. --]] function Slab.GetCurrentColumnIndex() return LayoutManager.GetCurrentColumnIndex() end --[[ SetScrollSpeed Sets the speed of scrolling when using the mouse wheel. Return: None. --]] function Slab.SetScrollSpeed(Speed) Region.SetWheelSpeed(Speed) end --[[ GetScrollSpeed Retrieves the speed of scrolling for the mouse wheel. Return: [Number] The current wheel scroll speed. --]] function Slab.GetScrollSpeed() return Region.GetWheelSpeed() end --[[ PushShader Pushes a shader effect to be applied to any following controls before a call to PopShader. Any shader effect that is still active will be cleared at the end of Slab's draw call. Shader: [Object] The shader object created with the love.graphics.newShader function. This object should be managed by the caller. Return: None. --]] function Slab.PushShader(Shader) DrawCommands.PushShader(Shader) end --[[ PopShader Pops the currently active shader effect. Will enable the next active shader on the stack. If none exists, no shader is applied. Return: None. --]] function Slab.PopShader() DrawCommands.PopShader() end --[[ EnableDocks Enables the docking functionality for a particular side of the viewport. List: [String/Table] A single item or list of items to enable for docking. The valid options are 'Left', 'Right', or 'Bottom'. Return: None. --]] function Slab.EnableDocks(List) Dock.Toggle(List, true) end --[[ DisableDocks Disables the docking functionality for a particular side of the viewport. List: [String/Table] A single item or list of items to disable for docking. The valid options are 'Left', 'Right', or 'Bottom'. Return: None. --]] function Slab.DisableDocks(List) Dock.Toggle(List, false) end --[[ SetDockOptions Set options for a dock type. Type: [String] The type of dock to set options for. This can be 'Left', 'Right', or 'Bottom'. Options: [Table] List of options that control how a dock behaves. NoSavedSettings: [Boolean] Flag to disable saving a dock's settings to the state INI file. --]] function Slab.SetDockOptions(Type, Options) Dock.SetOptions(Type, Options) end --[[ WindowToDoc Programatically set a window to dock. Type: [String] The type of dock to set options for. This can be 'Left', 'Right', or 'Bottom'. --]] function Slab.WindowToDock(Type) Window.ToDock(Type) end --[[ ToLoveFile Moves a file to a temporary location and returns a Love2D friendly way to access the file. The returned string can be used in any Love2D function that takes a Filename Source: [String] An absolute path to a file on the disk, can take a value from FileDialog Return: [String] A Love2D Filename ]] function Slab.ToLoveFile(Source) return FileSystem.ToLove(Source) end return Slab ================================================ FILE: Internal/Core/Config.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local FileSystem = require(SLAB_PATH .. '.Internal.Core.FileSystem') local Config = {} local DecodeValueFn = nil local Section = nil local function IsBasicType(Value) if Value ~= nil then local Type = type(Value) return Type == "number" or Type == "boolean" or Type == "string" end return false end local function IsArray(Table) if Table ~= nil and type(Table) == "table" then local N = 0 for K, V in pairs(Table) do if type(K) ~= "number" then return false end if not IsBasicType(V) then return false end N = N + 1 end return #Table == N end return false end local function EncodeValue(Value) local Result = "" if Value ~= nil then local Type = type(Value) if Type == "boolean" then Result = Value == true and "true" or "false" elseif Type == "number" or Type == "string" then Result = tostring(Value) end end return Result end local function EncodePair(Key, Value) local Result = tostring(Key) .. " = " if Value ~= nil then if type(Value) == "table" then if IsArray(Value) then Result = Result .. "(" .. table.concat(Value, ",") .. ")\n" else Result = Result .. "{" local First = true for K, V in pairs(Value) do if not First then Result = Result .. "," end Result = Result .. K .. "=" .. EncodeValue(V) First = false end Result = Result .. "}\n" end elseif IsBasicType(Value) then Result = Result .. tostring(Value) .. "\n" end end return Result end local function EncodeSection(Section, Values) local Result = "[" .. Section .. "]\n" for K, V in pairs(Values) do Result = Result .. EncodePair(K, V) end return Result .. "\n" end local function DecodeBoolean(Value) local Lower = string.lower(Value) if Lower == "true" then return true elseif Lower == "false" then return false end return nil end local function DecodeArray(Value) local Result = nil if string.sub(Value, 1, 1) == "(" then Result = {} local Index = 1 local Buffer = "" while Index <= #Value do local Ch = string.sub(Value, Index, Index) if Ch == ',' or Ch == ')' then local Item = DecodeValueFn(Buffer) if Item ~= nil then table.insert(Result, Item) end Buffer = "" elseif Ch ~= "(" and Ch ~= " " then Buffer = Buffer .. Ch end Index = Index + 1 end end return Result end local function DecodeTable(Value) local Result = nil if string.sub(Value, 1, 1) == "{" then Result = {} for K, V in string.gmatch(Value, "(%w+)=(%-?%w+)") do Result[K] = DecodeValueFn(V) end end return Result end local function DecodeValue(Value) if Value ~= nil and Value ~= "" then local Number = tonumber(Value) if Number ~= nil then return Number end local Boolean = DecodeBoolean(Value) if Boolean ~= nil then return Boolean end if Value == "nil" then return nil end local Array = DecodeArray(Value) if Array ~= nil then return Array end local Table = DecodeTable(Value) if Table ~= nil then return Table end return Value end return nil end DecodeValueFn = DecodeValue local function DecodeLine(Line, Result) if string.sub(Line, 1, 1) == ";" then return end if string.sub(Line, 1, 1) == "[" and string.sub(Line, #Line, #Line) == "]" then local Key = string.sub(Line, 2, #Line - 1) Result[Key] = {} Section = Result[Key] end local Index = string.find(Line, "=", 1, true) if Index ~= nil then local Key = string.sub(Line, 1, Index - 1) Key = string.gsub(Key, " ", "") local Value = string.sub(Line, Index + 1) Value = string.gsub(Value, " ", "") if string.sub(Value, #Value, #Value) == "," then Value = string.sub(Value, 1, #Value - 1) end if Section ~= nil then Section[Key] = DecodeValue(Value) else Result[Key] = DecodeValue(Value) end end end function Config.Encode(Table) local Result = "" if type(Table) == "table" and not IsArray(Table) then local Sections = {} for K, V in pairs(Table) do if type(V) == "table" and not IsArray(V) then Sections[K] = V else Result = Result .. EncodePair(K, V) end end if string.len(Result) > 0 then Result = Result .. "\n" end for K, V in pairs(Sections) do Result = Result .. EncodeSection(K, V) end end return Result end function Config.Decode(Stream) local Result = nil local Error = "" if Stream ~= nil then if type(Stream) == "string" then Result = {} local Start = 1 local End = string.find(Stream, "\n", Start, true) local Line = "" while End ~= nil do Line = string.sub(Stream, Start, End - 1) DecodeLine(Line, Result) Start = End + 1 End = string.find(Stream, "\n", Start, true) end Line = string.sub(Stream, Start) DecodeLine(Line, Result) else Error = "Invalid type given for Stream. Type given is " .. type(Stream) .. "." end else Error = "Invalid stream given to Config.Decode!" end return Result, Error end function Config.LoadFile(Path, IsDefault) local Result = nil local Contents, Error = FileSystem.ReadContents(Path, nil, IsDefault) if Contents ~= nil then Result, Error = Config.Decode(Contents) end return Result, Error end function Config.Save(Path, Table, IsDefault) local Result, Error = false if Table ~= nil then local Contents = Config.Encode(Table) Result, Error = FileSystem.SaveContents(Path, Contents, IsDefault) else Error = "Invalid table given to Config.Save!" end return Result, Error end return Config ================================================ FILE: Internal/Core/Cursor.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Cursor = {} local min = math.min local max = math.max local State = { X = 0.0, Y = 0.0, PrevX = 0.0, PrevY = 0.0, AnchorX = 0.0, AnchorY = 0.0, ItemX = 0.0, ItemY = 0.0, ItemW = 0.0, ItemH = 0.0, PadX = 4.0, PadY = 4.0, NewLineSize = 16.0, LineY = 0.0, LineH = 0.0, PrevLineY = 0.0, PrevLineH = 0.0 } local Stack = {} function Cursor.SetPosition(X, Y) State.PrevX = State.X State.PrevY = State.Y State.X = X State.Y = Y end function Cursor.SetX(X) State.PrevX = State.X State.X = X end function Cursor.SetY(Y) State.PrevY = State.Y State.Y = Y end function Cursor.SetRelativePosition(X, Y) State.PrevX = State.X State.PrevY = State.Y State.X = State.AnchorX + X State.Y = State.AnchorY + Y end function Cursor.SetRelativeX(X) State.PrevX = State.X State.X = State.AnchorX + X end function Cursor.SetRelativeY(Y) State.PrevY = State.Y State.Y = State.AnchorY + Y end function Cursor.AdvanceX(X) State.PrevX = State.X State.X = State.X + X + State.PadX end function Cursor.AdvanceY(Y) State.X = State.AnchorX State.PrevY = State.Y State.Y = State.Y + Y + State.PadY State.PrevLineY = State.LineY State.PrevLineH = State.LineH State.LineY = 0.0 State.LineH = 0.0 end function Cursor.SetAnchor(X, Y) State.AnchorX = X State.AnchorY = Y end function Cursor.SetAnchorX(X) State.AnchorX = X end function Cursor.SetAnchorY(Y) State.AnchorY = Y end function Cursor.GetAnchor() return State.AnchorX, State.AnchorY end function Cursor.GetAnchorX() return State.AnchorX end function Cursor.GetAnchorY() return State.AnchorY end function Cursor.GetPosition() return State.X, State.Y end function Cursor.GetX() return State.X end function Cursor.GetY() return State.Y end function Cursor.GetRelativePosition() return Cursor.GetRelativeX(), Cursor.GetRelativeY() end function Cursor.GetRelativeX() return State.X - State.AnchorX end function Cursor.GetRelativeY() return State.Y - State.AnchorY end function Cursor.SetItemBounds(X, Y, W, H) State.ItemX = X State.ItemY = Y State.ItemW = W State.ItemH = H if State.LineY == 0.0 then State.LineY = Y end State.LineY = min(State.LineY, Y) State.LineH = max(State.LineH, H) end function Cursor.GetItemBounds() return State.ItemX, State.ItemY, State.ItemW, State.ItemH end function Cursor.IsInItemBounds(X, Y) return State.ItemX <= X and X <= State.ItemX + State.ItemW and State.ItemY <= Y and Y <= State.ItemY + State.ItemH end function Cursor.SameLine(Options) Options = Options == nil and {} or Options Options.Pad = Options.Pad == nil and 0.0 or Options.Pad Options.CenterY = Options.CenterY == nil and false or Options.CenterY State.LineY = State.PrevLineY State.LineH = State.PrevLineH State.X = State.ItemX + State.ItemW + State.PadX + Options.Pad State.Y = State.PrevY if Options.CenterY then State.Y = State.Y + (State.LineH * 0.5) - (State.NewLineSize * 0.5) end end function Cursor.SetNewLineSize(NewLineSize) State.NewLineSize = NewLineSize end function Cursor.GetNewLineSize() return State.NewLineSize end function Cursor.NewLine() Cursor.AdvanceY(State.NewLineSize) end function Cursor.GetLineHeight() return State.PrevLineH end function Cursor.PadX() return State.PadX end function Cursor.PadY() return State.PadY end function Cursor.Indent(Width) State.AnchorX = State.AnchorX + Width State.X = State.AnchorX end function Cursor.Unindent(Width) Cursor.Indent(-Width) end function Cursor.PushContext() table.insert(Stack, 1, Utility.Copy(State)) end function Cursor.PopContext() if #Stack == 0 then return end State = table.remove(Stack, 1) end return Cursor ================================================ FILE: Internal/Core/DrawCommands.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Stats = require(SLAB_PATH .. ".Internal.Core.Stats") local TablePool = require(SLAB_PATH .. ".Internal.Core.TablePool") local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local insert = table.insert local remove = table.remove local sin = math.sin local cos = math.cos local rad = math.rad local max = math.max local min = math.min local graphics = love.graphics local DrawCommands = {} local PendingBatches = {} local ActiveBatch = nil local Shaders = {} local EMPTY = {} local BLACK = { 0, 0, 0, 1 } local WHITE = { 1, 1, 1, 1 } local TypeRect = 1 local TypeTriangle = 2 local TypeText = 3 local TypeScissor = 4 local TypeTransformPush = 5 local TypeTransformPop = 6 local TypeApplyTransform = 7 local TypeCheck = 8 local TypeLine = 9 local TypeTextFormatted = 10 local TypeIntersectScissor = 11 local TypeCross = 12 local TypeImage = 13 local TypeSubImage = 14 local TypeCircle = 15 local TypeDrawCanvas = 16 local TypeMesh = 17 local TypeTextObject = 18 local TypeCurve = 19 local TypePolygon = 20 local TypeShaderPush = 21 local TypeShaderPop = 22 local LayerNormal = 1 local LayerDock = 2 local LayerContextMenu = 3 local LayerMainMenuBar = 4 local LayerDialog = 5 local LayerDebug = 6 local LayerMouse = 7 local LayerNames = { Normal = LayerNormal, Dock = LayerDock, ContextMenu = LayerContextMenu, MainMenuBar = LayerMainMenuBar, Dialog = LayerDialog, Debug = LayerDebug, Mouse = LayerMouse, } local LayerTable = { {}, {}, {}, {}, {}, {}, {} } local ActiveLayer = LayerNormal local StatsCategory = 'Slab Draw' local pool = {} for i = TypeRect, TypeShaderPop do pool[i] = TablePool() end local function AddArc(Verts, CenterX, CenterY, Radius, Angle1, Angle2, Segments, X, Y) if Radius == 0 then insert(Verts, CenterX + X) insert(Verts, CenterY + Y) return end local Step = (Angle2 - Angle1) / Segments for Theta = Angle1, Angle2, Step do local Radians = rad(Theta) insert(Verts, sin(Radians) * Radius + CenterX + X) insert(Verts, cos(Radians) * Radius + CenterY + Y) end end local function DrawRect(Rect) local StatHandle = Stats.Begin('DrawRect', StatsCategory) local LineW = graphics.getLineWidth() graphics.setLineWidth(Rect.LineW) graphics.setColor(Rect.Color) local pixelOffset = Rect.Mode == 'line' and .5 or 0 graphics.rectangle(Rect.Mode, Rect.X + pixelOffset, Rect.Y + pixelOffset, Rect.Width, Rect.Height, Rect.Radius, Rect.Radius) graphics.setLineWidth(LineW) Stats.End(StatHandle) end local function GetTriangleVertices(X, Y, Radius, Rotation) local Radians = rad(Rotation) local cs, sn = cos(Radians), sin(Radians) local X1, Y1 = 0, -Radius local X2, Y2 = -Radius, Radius local X3, Y3 = Radius, Radius local PX1 = X1 * cs - Y1 * sn local PY1 = Y1 * cs + X1 * sn local PX2 = X2 * cs - Y2 * sn local PY2 = Y2 * cs + X2 * sn local PX3 = X3 * cs - Y3 * sn local PY3 = Y3 * cs + X3 * sn return X + PX1, Y + PY1, X + PX2, Y + PY2, X + PX3, Y + PY3 end local function DrawTriangle(Triangle) local StatHandle = Stats.Begin('DrawTriangle', StatsCategory) graphics.setColor(Triangle.Color) graphics.polygon(Triangle.Mode, GetTriangleVertices(Triangle.X, Triangle.Y, Triangle.Radius, Triangle.Rotation)) Stats.End(StatHandle) end local function DrawCheck(Check) local StatHandle = Stats.Begin('DrawCheck', StatsCategory) graphics.setColor(Check.Color) graphics.line( Check.X - Check.Radius * 0.5, Check.Y, Check.X, Check.Y + Check.Radius, Check.X + Check.Radius, Check.Y - Check.Radius ) Stats.End(StatHandle) end local function DrawText(Text) local StatHandle = Stats.Begin('DrawText', StatsCategory) graphics.setFont(Text.Font) graphics.setColor(Text.Color) graphics.print(Text.Text, Text.X, Text.Y) Stats.End(StatHandle) end local function DrawTextFormatted(Text) local StatHandle = Stats.Begin('DrawTextFormatted', StatsCategory) graphics.setFont(Text.Font) graphics.setColor(Text.Color) graphics.printf(Text.Text, Text.X, Text.Y, Text.W, Text.Align) Stats.End(StatHandle) end local function DrawTextObject(Text) local StatHandle = Stats.Begin('DrawTextObject', StatsCategory) graphics.setColor(1, 1, 1, 1) graphics.draw(Text.Text, Text.X, Text.Y) Stats.End(StatHandle) end local function DrawLine(Line) local StatHandle = Stats.Begin('DrawLine', StatsCategory) graphics.setColor(Line.Color) local LineW = graphics.getLineWidth() graphics.setLineWidth(Line.Width) graphics.line(Line.X1, Line.Y1, Line.X2, Line.Y2) graphics.setLineWidth(LineW) Stats.End(StatHandle) end local function DrawCross(Cross) local StatHandle = Stats.Begin('DrawCross', StatsCategory) local X, Y = Cross.X, Cross.Y local R = Cross.Radius graphics.setColor(Cross.Color) graphics.line(X - R, Y - R, X + R, Y + R) graphics.line(X - R, Y + R, X + R, Y - R) Stats.End(StatHandle) end local function DrawImage(Image) local StatHandle = Stats.Begin('DrawImage', StatsCategory) graphics.setColor(Image.Color) graphics.draw(Image.Image, Image.X, Image.Y, Image.Rotation, Image.ScaleX, Image.ScaleY) Stats.End(StatHandle) end local function DrawSubImage(Image) local StatHandle = Stats.Begin('DrawSubImage', StatsCategory) graphics.setColor(Image.Color) graphics.draw(Image.Image, Image.Quad, Image.Transform) Stats.End(StatHandle) end local function DrawCircle(Circle) local StatHandle = Stats.Begin('DrawCircle', StatsCategory) graphics.setColor(Circle.Color) graphics.circle(Circle.Mode, Circle.X, Circle.Y, Circle.Radius, Circle.Segments) Stats.End(StatHandle) end local function DrawCurve(Curve) local StatHandle = Stats.Begin('DrawCurve', StatsCategory) graphics.setColor(Curve.Color) graphics.line(Curve.Points) Stats.End(StatHandle) end local function DrawPolygon(Polygon) local StatHandle = Stats.Begin('DrawPolygon', StatsCategory) graphics.setColor(Polygon.Color) graphics.polygon(Polygon.Mode, Polygon.Points) Stats.End(StatHandle) end local function DrawCanvas(Canvas) local StatHandle = Stats.Begin('DrawCanvas', StatsCategory) graphics.setBlendMode('alpha', 'premultiplied') graphics.setColor(1.0, 1.0, 1.0, 1.0) graphics.draw(Canvas.Canvas, Canvas.X, Canvas.Y) graphics.setBlendMode('alpha') Stats.End(StatHandle) end local function DrawMesh(Mesh) local StatHandle = Stats.Begin('DrawMesh', StatsCategory) graphics.setColor(1.0, 1.0, 1.0, 1.0) graphics.draw(Mesh.Mesh, Mesh.X, Mesh.Y) Stats.End(StatHandle) end local function ShaderPush(shader) insert(Shaders, 1, shader.Shader) graphics.setShader(shader.Shader) end local function ShaderPop() remove(Shaders, 1) graphics.setShader(Shaders[1]) end local function SetScissor(rect) graphics.setScissor(rect.X, rect.Y, rect.W, rect.H) end local function TransformPush() graphics.push() end local function TransformPop() graphics.pop() end local function ApplyTransform(transform) graphics.applyTransform(transform.Transform) end local function IntersectScissor(rect) graphics.intersectScissor(rect.X, rect.Y, rect.W, rect.H) end local DRAWTYPES = { DrawRect, DrawTriangle, DrawText, SetScissor, TransformPush, TransformPop, ApplyTransform, DrawCheck, DrawLine, DrawTextFormatted, IntersectScissor, DrawCross, DrawImage, DrawSubImage, DrawCircle, DrawCanvas, DrawMesh, DrawTextObject, DrawCurve, DrawPolygon, ShaderPush, ShaderPop, } local function DrawElements(Elements) local StatHandle = Stats.Begin('Draw Elements', StatsCategory) for i = 1, #Elements do local element = Elements[i] DRAWTYPES[element.Type](element) end Stats.End(StatHandle) end local function DrawChannel(channel) for i = 1, #channel do DrawElements(channel[i]) end end local function ClearBatch(batch) for i = 1, #batch do pool[batch[i].Type]:push(batch[i]) batch[i] = nil end end local function AssertActiveBatch() assert(ActiveBatch ~= nil, "DrawCommands.Begin was not called before commands were issued!") end local function DrawLayer(Layer, Name) if Layer == nil then return end local StatHandle = Stats.Begin('Draw Layer ' .. Name, StatsCategory) local minChannel, maxChannel = 1e9, 0 for i in pairs(Layer) do minChannel, maxChannel = min(minChannel, i), max(maxChannel, i) end for i = minChannel, maxChannel do if Layer[i] then DrawChannel(Layer[i]) end end Stats.End(StatHandle) end function DrawCommands.Reset() for i = LayerNormal, LayerMouse do local layer = LayerTable[i] for j, channel in pairs(layer) do for i, batch in ipairs(channel) do ClearBatch(batch) end layer[j] = nil end end ActiveLayer = LayerNormal ActiveBatch = nil for i in ipairs(Shaders) do Shaders[i] = nil end end function DrawCommands.Begin(channel) local layer = LayerTable[ActiveLayer] channel = channel or 1 if layer[channel] == nil then layer[channel] = {} end ActiveBatch = {} insert(layer[channel], ActiveBatch) insert(PendingBatches, ActiveBatch) end function DrawCommands.End(clearElements) if ActiveBatch == nil then return end if clearElements then ClearBatch(ActiveBatch) end graphics.setScissor() remove(PendingBatches) ActiveBatch = PendingBatches[#PendingBatches] end function DrawCommands.SetLayer(Layer) ActiveLayer = LayerNames[Layer] end function DrawCommands.Rectangle(Mode, X, Y, Width, Height, Color, Radius, Segments, LineW) AssertActiveBatch() if type(Radius) == 'table' then Segments = Segments == nil and 10 or Segments local Verts = {} local TL = Radius[1] local TR = Radius[2] local BR = Radius[3] local BL = Radius[4] TL = TL == nil and 0 or TL TR = TR == nil and 0 or TR BR = BR == nil and 0 or BR BL = BL == nil and 0 or BL AddArc(Verts, Width - BR, Height - BR, BR, 0, 90, Segments, X, Y) AddArc(Verts, Width - TR, TR, TR, 90, 180, Segments, X, Y) AddArc(Verts, TL, TL, TL, 180, 270, Segments, X, Y) AddArc(Verts, BL, Height - BL, BL, 270, 360, Segments, X, Y) DrawCommands.Polygon(Mode, Verts, Color) else local Item = pool[TypeRect]:pull() Item.Type = TypeRect Item.Mode = Mode Item.X = X Item.Y = Y Item.Width = Width Item.Height = Height Item.Color = Color or BLACK Item.Radius = Radius or 0 Item.LineW = LineW or graphics.getLineWidth() insert(ActiveBatch, Item) end end function DrawCommands.Triangle(Mode, X, Y, Radius, Rotation, Color) AssertActiveBatch() local Item = pool[TypeTriangle]:pull() Item.Type = TypeTriangle Item.Mode = Mode Item.X = X Item.Y = Y Item.Radius = Radius Item.Rotation = Rotation Item.Color = Color or BLACK insert(ActiveBatch, Item) end function DrawCommands.Print(Text, X, Y, Color, Font) AssertActiveBatch() local Item = pool[TypeText]:pull() Item.Type = TypeText Item.Text = Text Item.X = X Item.Y = Y Item.Color = Color or WHITE Item.Font = Font insert(ActiveBatch, Item) end function DrawCommands.Printf(Text, X, Y, W, Align, Color, Font) AssertActiveBatch() local Item = pool[TypeTextFormatted]:pull() Item.Type = TypeTextFormatted Item.Text = Text Item.X = X Item.Y = Y Item.W = W Item.Align = Align or 'left' Item.Color = Color or WHITE Item.Font = Font insert(ActiveBatch, Item) end function DrawCommands.Scissor(X, Y, W, H) AssertActiveBatch() if W ~= nil then W = max(W, 0.0) end if H ~= nil then H = max(H, 0.0) end local SF = Scale.GetScale() local Item = pool[TypeScissor]:pull() Item.Type = TypeScissor if X then X = X * SF end if Y then Y = Y * SF end if W then W = W * SF end if H then H = H * SF end Item.X = X Item.Y = Y Item.W = W Item.H = H insert(ActiveBatch, Item) end function DrawCommands.IntersectScissor(X, Y, W, H) AssertActiveBatch() if W ~= nil then W = max(W, 0.0) end if H ~= nil then H = max(H, 0.0) end local SF = Scale.GetScale() local Item = pool[TypeIntersectScissor]:pull() Item.Type = TypeIntersectScissor Item.X = (X or 0.0) * SF Item.Y = (Y or 0.0) * SF Item.W = (W or 0.0) * SF Item.H = (H or 0.0) * SF insert(ActiveBatch, Item) end function DrawCommands.TransformPush() AssertActiveBatch() local Item = pool[TypeTransformPush]:pull() Item.Type = TypeTransformPush insert(ActiveBatch, Item) end function DrawCommands.TransformPop() AssertActiveBatch() local Item = pool[TypeTransformPop]:pull() Item.Type = TypeTransformPop insert(ActiveBatch, Item) end function DrawCommands.ApplyTransform(Transform) AssertActiveBatch() local Item = pool[TypeApplyTransform]:pull() Item.Type = TypeApplyTransform Item.Transform = Transform insert(ActiveBatch, Item) end function DrawCommands.Check(X, Y, Radius, Color) AssertActiveBatch() local Item = pool[TypeCheck]:pull() Item.Type = TypeCheck Item.X = X Item.Y = Y Item.Radius = Radius Item.Color = Color or BLACK insert(ActiveBatch, Item) end function DrawCommands.Line(X1, Y1, X2, Y2, Width, Color) AssertActiveBatch() local Item = pool[TypeLine]:pull() Item.Type = TypeLine Item.X1 = X1 Item.Y1 = Y1 Item.X2 = X2 Item.Y2 = Y2 Item.Width = Width Item.Color = Color or BLACK insert(ActiveBatch, Item) end function DrawCommands.Cross(X, Y, Radius, Color) AssertActiveBatch() local Item = pool[TypeCross]:pull() Item.Type = TypeCross Item.X = X Item.Y = Y Item.Radius = Radius Item.Color = Color or BLACK insert(ActiveBatch, Item) end function DrawCommands.Image(X, Y, Image, Rotation, ScaleX, ScaleY, Color) AssertActiveBatch() local Item = pool[TypeImage]:pull() Item.Type = TypeImage Item.X = X Item.Y = Y Item.Image = Image Item.Rotation = Rotation Item.ScaleX = ScaleX Item.ScaleY = ScaleY Item.Color = Color or WHITE insert(ActiveBatch, Item) end function DrawCommands.SubImage(X, Y, Image, SX, SY, SW, SH, Rotation, ScaleX, ScaleY, Color) AssertActiveBatch() local Item = pool[TypeSubImage]:pull() Item.Type = TypeSubImage Item.Transform = love.math.newTransform(X, Y, Rotation, ScaleX, ScaleY) Item.Image = Image Item.Quad = graphics.newQuad(SX, SY, SW, SH, Image:getWidth(), Image:getHeight()) Item.Color = Color or WHITE insert(ActiveBatch, Item) end function DrawCommands.Circle(Mode, X, Y, Radius, Color, Segments) AssertActiveBatch() local Item = pool[TypeCircle]:pull() Item.Type = TypeCircle Item.Mode = Mode Item.X = X Item.Y = Y Item.Radius = Radius Item.Color = Color or BLACK Item.Segments = Segments and Segments or 24 insert(ActiveBatch, Item) end function DrawCommands.DrawCanvas(Canvas, X, Y) AssertActiveBatch() local Item = pool[TypeDrawCanvas]:pull() Item.Type = TypeDrawCanvas Item.Canvas = Canvas Item.X = X Item.Y = Y insert(ActiveBatch, Item) end function DrawCommands.Mesh(Mesh, X, Y) AssertActiveBatch() local Item = pool[TypeMesh]:pull() Item.Type = TypeMesh Item.Mesh = Mesh Item.X = X Item.Y = Y insert(ActiveBatch, Item) end function DrawCommands.Text(Text, X, Y) AssertActiveBatch() local Item = pool[TypeTextObject]:pull() Item.Type = TypeTextObject Item.Text = Text Item.X = X Item.Y = Y Item.Color = BLACK insert(ActiveBatch, Item) end function DrawCommands.Curve(Points, Color) AssertActiveBatch() local Item = pool[TypeCurve]:pull() Item.Type = TypeCurve Item.Points = Points Item.Color = Color or BLACK insert(ActiveBatch, Item) end function DrawCommands.Polygon(Mode, Points, Color) AssertActiveBatch() local Item = pool[TypePolygon]:pull() Item.Type = TypePolygon Item.Mode = Mode Item.Points = Points Item.Color = Color or BLACK insert(ActiveBatch, Item) end function DrawCommands.PushShader(Shader) AssertActiveBatch() local Item = pool[TypeShaderPush]:pull() Item.Type = TypeShaderPush Item.Shader = Shader insert(ActiveBatch, Item) end function DrawCommands.PopShader() AssertActiveBatch() local Item = pool[TypeShaderPop]:pull() Item.Type = TypeShaderPop insert(ActiveBatch, Item) end function DrawCommands.Execute() local StatHandle = Stats.Begin('Execute', StatsCategory) graphics.scale(Scale.GetScale()) DrawLayer(LayerTable[LayerNormal], 'Normal') DrawLayer(LayerTable[LayerDock], 'Dock') DrawLayer(LayerTable[LayerContextMenu], 'ContextMenu') DrawLayer(LayerTable[LayerMainMenuBar], 'MainMenuBar') DrawLayer(LayerTable[LayerDialog], 'Dialog') DrawLayer(LayerTable[LayerDebug], 'Debug') DrawLayer(LayerTable[LayerMouse], 'Mouse') graphics.setShader() Stats.End(StatHandle) end local function GetLayerDebugInfo(Layer) local Result = {} Result['Channel Count'] = #Layer local Channels = {} for K, Channel in pairs(Layer) do local Collection = {} Collection['Batch Count'] = #Channel insert(Channels, Collection) end Result['Channels'] = Channels return Result end function DrawCommands.GetDebugInfo() local Result = {} Result['Normal'] = GetLayerDebugInfo(LayerTable[LayerNormal]) Result['Dock'] = GetLayerDebugInfo(LayerTable[LayerDock]) Result['ContextMenu'] = GetLayerDebugInfo(LayerTable[LayerContextMenu]) Result['MainMenuBar'] = GetLayerDebugInfo(LayerTable[LayerMainMenuBar]) Result['Dialog'] = GetLayerDebugInfo(LayerTable[LayerDialog]) Result['Debug'] = GetLayerDebugInfo(LayerTable[LayerDebug]) Result['Mouse'] = GetLayerDebugInfo(LayerTable[LayerMouse]) return Result end return DrawCommands ================================================ FILE: Internal/Core/FileSystem.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local FileSystem = {} local Syscalls = {} local Bit = require('bit') local FFI = require('ffi') local function ShouldFilter(Name, Filter) Filter = Filter == nil and "*.*" or Filter local Extension = FileSystem.GetExtension(Name) if Filter ~= "*.*" then local FilterExt = FileSystem.GetExtension(Filter) if Extension ~= FilterExt then return true end end return false end local GetDirectoryItems = nil local Exists = nil local IsDirectory = nil local Copy = nil local function Access(Table, Param) return Table[Param]; end local function ErrorAtAccess(Table, Param) return not pcall(Access, Table, Param); end --[[ The following code is based on the following sources: LoveFS v1.1 https://github.com/linux-man/lovefs Pure Lua FileSystem Access Under the MIT license. copyright(c) 2016 Caldas Lopes aka linux-man luapower/fs_posix https://github.com/luapower/fs portable filesystem API for LuaJIT / Linux & OSX backend Written by Cosmin Apreutesei. Public Domain. --]] if FFI.os == "Windows" then FFI.cdef[[ #pragma pack(push) #pragma pack(1) struct WIN32_FIND_DATAW { uint32_t dwFileAttributes; uint64_t ftCreationTime; uint64_t ftLastAccessTime; uint64_t ftLastWriteTime; uint32_t dwReserved[4]; wchar_t cFileName[520]; wchar_t cAlternateFileName[28]; }; #pragma pack(pop) typedef unsigned long DWORD; static const DWORD FILE_ATTRIBUTE_DIRECTORY = 0x10; static const DWORD INVALID_FILE_ATTRIBUTES = -1; void* FindFirstFileW(const wchar_t* pattern, struct WIN32_FIND_DATAW* fd); bool FindNextFileW(void* ff, struct WIN32_FIND_DATAW* fd); bool FindClose(void* ff); DWORD GetFileAttributesW(const wchar_t* Path); bool CopyFileW(const wchar_t* src, const wchar_t* dst, bool bFailIfExists); int MultiByteToWideChar(unsigned int CodePage, uint32_t dwFlags, const char* lpMultiByteStr, int cbMultiByte, const wchar_t* lpWideCharStr, int cchWideChar); int WideCharToMultiByte(unsigned int CodePage, uint32_t dwFlags, const wchar_t* lpWideCharStr, int cchWideChar, const char* lpMultiByteStr, int cchMultiByte, const char* default, int* used); ]] local WIN32_FIND_DATA = FFI.typeof('struct WIN32_FIND_DATAW') local INVALID_HANDLE = FFI.cast('void*', -1) local function u2w(str, code) local size = FFI.C.MultiByteToWideChar(code or 65001, 0, str, #str, nil, 0) local buf = FFI.new("wchar_t[?]", size * 2 + 2) FFI.C.MultiByteToWideChar(code or 65001, 0, str, #str, buf, size * 2) return buf end local function w2u(wstr, code) local size = FFI.C.WideCharToMultiByte(code or 65001, 0, wstr, -1, nil, 0, nil, nil) local buf = FFI.new("char[?]", size + 1) size = FFI.C.WideCharToMultiByte(code or 65001, 0, wstr, -1, buf, size, nil, nil) return FFI.string(buf) end GetDirectoryItems = function(Directory, Options) local Result = {} local FindData = FFI.new(WIN32_FIND_DATA) local Handle = FFI.C.FindFirstFileW(u2w(Directory .. "\\*"), FindData) FFI.gc(Handle, FFI.C.FindClose) if Handle ~= nil then repeat local Name = w2u(FindData.cFileName) if Name ~= "." and Name ~= ".." then local AddDirectory = (FindData.dwFileAttributes == 16 or FindData.dwFileAttributes == 17) and Options.Directories local AddFile = FindData.dwFileAttributes == 32 and Options.Files if (AddDirectory or AddFile) and not ShouldFilter(Name, Options.Filter) then table.insert(Result, Name) end end until not FFI.C.FindNextFileW(Handle, FindData) end FFI.C.FindClose(FFI.gc(Handle, nil)) return Result end Exists = function(Path) local Attributes = FFI.C.GetFileAttributesW(u2w(Path)) return Attributes ~= FFI.C.INVALID_FILE_ATTRIBUTES end IsDirectory = function(Path) local Attributes = FFI.C.GetFileAttributesW(u2w(Path)) return Attributes ~= FFI.C.INVALID_FILE_ATTRIBUTES and Bit.band(Attributes, FFI.C.FILE_ATTRIBUTE_DIRECTORY) ~= 0 end Copy = function(Source, Dest) FFI.C.CopyFileW(u2w(Source), u2w(Dest), false) end else FFI.cdef[[ typedef struct DIR DIR; typedef size_t time_t; static const int S_IFREG = 0x8000; static const int S_IFDIR = 0x4000; DIR* opendir(const char* name); int closedir(DIR* dirp); ]] if FFI.os == "OSX" then FFI.cdef[[ struct dirent { uint64_t d_ino; uint64_t d_off; uint16_t d_reclen; uint16_t d_namlen; uint8_t d_type; char d_name[1024]; }; struct stat { uint32_t st_dev; uint16_t st_mode; uint16_t st_nlink; uint64_t st_ino; uint32_t st_uid; uint32_t st_gid; uint32_t st_rdev; time_t st_atime; long st_atime_nsec; time_t st_mtime; long st_mtime_nsec; time_t st_ctime; long st_ctime_nsec; time_t st_btime; long st_btime_nsec; int64_t st_size; int64_t st_blocks; int32_t st_blksize; uint32_t st_flags; uint32_t st_gen; int32_t st_lspare; int64_t st_qspare[2]; }; struct dirent* readdir(DIR* dirp) asm("readdir$INODE64"); int stat64(const char* path, struct stat* buf); ]] else FFI.cdef[[ struct dirent { uint64_t d_ino; int64_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[256]; }; struct stat { uint64_t st_dev; uint64_t st_ino; uint64_t st_nlink; uint32_t st_mode; uint32_t st_uid; uint32_t st_gid; uint32_t __pad0; uint64_t st_rdev; int64_t st_size; int64_t st_blksize; int64_t st_blocks; uint64_t st_atime; uint64_t st_atime_nsec; uint64_t st_mtime; uint64_t st_mtime_nsec; uint64_t st_ctime; uint64_t st_ctime_nsec; int64_t __unused[3]; }; struct dirent* readdir(DIR* dirp) asm("readdir64"); int syscall(int number, ...); int stat64(const char* path, struct stat* buf); ]] end local Stat = FFI.typeof('struct stat'); if FFI.arch == "x86" then Syscalls.SYS_stat = 106 elseif FFI.arch == "arm" and FFI.abi("eabi") then Syscalls.SYS_stat = 106 elseif FFI.arch == "x64" then Syscalls.SYS_stat = 4 end if(ErrorAtAccess(FFI.C, "stat64")) then local function SysStat(Path, Buffer) return FFI.C.syscall(Syscalls.SYS_stat, Path, Buffer) end Exists = function(Path) local Buffer = Stat() return SysStat(Path, Buffer) == 0 end IsDirectory = function(Path) local Buffer = Stat() if SysStat(Path, Buffer) == 0 then return Bit.band(Buffer.st_mode, 0xf000) == FFI.C.S_IFDIR end return false end else Exists = function(Path) local Buffer = Stat() return FFI.C.stat64(Path, Buffer) == 0 end IsDirectory = function(Path) local Buffer = Stat() if FFI.C.stat64(Path, Buffer) == 0 then return Bit.band(Buffer.st_mode, 0xf000) == FFI.C.S_IFDIR end return false end end GetDirectoryItems = function(Directory, Options) local Result = {} local DIR = FFI.C.opendir(Directory) if DIR ~= nil then local Entry = FFI.C.readdir(DIR) while Entry ~= nil do local Name = FFI.string(Entry.d_name) if Name ~= "." and Name ~= ".." and string.sub(Name, 1, 1) ~= "." then local AddDirectory = Entry.d_type == 4 and Options.Directories local AddFile = Entry.d_type == 8 and Options.Files if (AddDirectory or AddFile) and not ShouldFilter(Name, Options.Filter) then table.insert(Result, Name) end end Entry = FFI.C.readdir(DIR) end FFI.C.closedir(DIR) end return Result end Copy = function(Source, Dest) local inp = assert(io.open(Source, "rb")) local out = assert(io.open(Dest, "wb")) local data = inp:read("*all") out:write(data) assert(out:close()) end end function FileSystem.Separator() -- Lua/Love2D returns all paths with back slashes. return "/" end function FileSystem.GetDirectoryItems(Directory, Options) Options = Options == nil and {} or Options Options.Files = Options.Files == nil and true or Options.Files Options.Directories = Options.Directories == nil and true or Options.Directories Options.Filter = Options.Filter == nil and "*.*" or Options.Filter if string.sub(Directory, #Directory, #Directory) ~= FileSystem.Separator() then Directory = Directory .. FileSystem.Separator() end local Result = GetDirectoryItems(Directory, Options) table.sort(Result) return Result end function FileSystem.Exists(Path) return Exists(Path) end function FileSystem.IsDirectory(Path) return IsDirectory(Path) end function FileSystem.Parent(Path) local Result = Path local Index = 1 local I = Index repeat Index = I I = string.find(Path, FileSystem.Separator(), Index + 1, true) until I == nil if Index > 1 then Result = string.sub(Path, 1, Index - 1) end return Result end --[[ IsAbsolute Determines if the given path is an absolute path or a relative path. This is determined by checking if the path starts with a drive letter on Windows, or the Unix root character '/'. Path: [String] The path to check. Return: [Boolean] True if the path is absolute, false if it is relative. --]] function FileSystem.IsAbsolute(Path) if Path == nil or Path == "" then return false end if FFI.os == "Windows" then return string.match(Path, "(.:-)\\") ~= nil end return string.sub(Path, 1, 1) == FileSystem.Separator() end --[[ GetDrive Attempts to retrieve the drive letter from the given absolute path. This function is targeted for paths on Windows. Unix style paths will just return the root '/'. Path: [String] The absolute path containing the drive letter. Return: [String] The drive letter, colon, and path separator are returned. On Unix platforms, just the '/' character is returned. --]] function FileSystem.GetDrive(Path) if not FileSystem.IsAbsolute(Path) then return "" end if FFI.os == "Windows" then local Result = string.match(Path, "(.:-)\\") if Result == nil then Result = string.match(Path, "(.:-)" .. FileSystem.Separator()) end if Result ~= nil then return Result .. FileSystem.Separator() end end return FileSystem.Separator() end --[[ Determines if the given path is a drive letter on Windows or the root directory on Unix. Path: [String] The absolute path containing the drive letter. Return: [Boolean] True if the given path is a drive. --]] function FileSystem.IsDrive(Path) if Path == nil then return false end return FileSystem.GetDrive(Path) == Path end --[[ Sanitize This function will attempt to remove any '.' or '..' components in the path and will appropriately modify the result to represent changes to the path based on if a '..' component is found. This function will keep the path's scope (relative/absolute) during sanitization. Path: [String] The path to be sanitized. Return: [String] The sanitized path string. --]] function FileSystem.Sanitize(Path) local Result = "" local Items = {} for Item in string.gmatch(Path, "([^" .. FileSystem.Separator() .. "]+)") do -- Always add the first item. If the given path is relative, then this will help preserve that. if #Items == 0 then table.insert(Items, Item) else -- If the parent directory item is found, pop the last item off of the stack. if Item == ".." then table.remove(Items, #Items) -- Ignore same directory item and push the item to the stack. elseif Item ~= "." then table.insert(Items, Item) end end end for I, Item in ipairs(Items) do if Result == "" then if Item == "." or Item == ".." then Result = Item else if FileSystem.IsAbsolute(Path) then Result = FileSystem.GetDrive(Path) .. Item else Result = Item end end else Result = Result .. FileSystem.Separator() .. Item end end return Result end function FileSystem.GetBaseName(Path, RemoveExtension) local Result = string.match(Path, "^.+/(.+)$") if Result == nil then Result = Path end if RemoveExtension then Result = FileSystem.RemoveExtension(Result) end return Result end function FileSystem.GetDirectory(Path) local Result = string.match(Path, "(.+)/") if Result == nil then Result = Path end return Result end function FileSystem.GetRootDirectory(Path) local Result = Path local Index = string.find(Path, FileSystem.Separator(), 1, true) if Index ~= nil then Result = string.sub(Path, 1, Index - 1) end return Result end function FileSystem.GetSlabPath() local Path = love.filesystem.getSource() if not FileSystem.IsDirectory(Path) then Path = love.filesystem.getSourceBaseDirectory() end return Path .. "/Slab" end function FileSystem.GetExtension(Path) local Result = string.match(Path, "[^.]+$") if Result == nil then Result = "" end return Result end function FileSystem.RemoveExtension(Path) local Result = string.match(Path, "(.+)%.") if Result == nil then Result = Path end return Result end function FileSystem.ReadContents(Path, IsBinary, IsDefault) local Result, Error if IsDefault then Result, Error = love.filesystem.read(Path) else local Handle local Mode = IsBinary and "rb" or "r" Handle, Error = io.open(Path, Mode) if Handle ~= nil then Result = Handle:read("*a") Handle:close() end end return Result, Error end function FileSystem.SaveContents(Path, Contents, IsDefault) local Result, Error if IsDefault then Result, Error = love.filesystem.write(Path, Contents) else local Handle, Error = io.open(Path, "w") if Handle ~= nil then Handle:write(Contents) Handle:close() Result = true end end return Result, Error end function FileSystem.GetClipboard() local Contents = love.system.getClipboardText() if Contents ~= nil then -- Remove Windows style newlines. Contents = string.gsub(Contents, "\r\n", "\n") end return Contents end function FileSystem.ToLove(Source) local ext = FileSystem.GetExtension(Source) love.filesystem.createDirectory('tmp') Copy(Source, love.filesystem.getSaveDirectory() .. "/tmp/temp." .. ext) return "tmp/temp." .. ext end return FileSystem ================================================ FILE: Internal/Core/IdCache.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local IdCache = {} IdCache.__index = IdCache function IdCache:get(parentId, id) local pId = self._ids[parentId] local resultId = pId and pId[id] if resultId then return resultId end resultId = ("%s.%s"):format(parentId, id) if pId then pId[id] = resultId else self._ids[parentId] = { [id] = resultId } end return resultId end return function() return setmetatable({ _ids = {} }, IdCache) end ================================================ FILE: Internal/Core/Messages.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] --[[ The messages system is a system for Slab to notify developers of issues or suggestions on aspects of the API. Functions or options may be deprecated or Slab may offer an alternative usage. The system is designed to notify the user only once to prevent any repeated output to the console if enabled. This system can be enabled at startup and the developer will have the ability to gather the messages to be displayed in a control if desired. --]] local insert = table.insert local Messages = {} local Enabled = true local Cache = {} function Messages.Broadcast(Id, Message) if not Enabled then return end assert(Id ~= nil, "Id is invalid.") assert(type(Id) == 'string', "Id is not a string type.") assert(Message ~= nil, "Message is invalid.") assert(type(Message) == 'string', "Message is not a string type.") if Cache[Id] == nil then Cache[Id] = Message print(Message) end end function Messages.Get() local Result = {} for K, V in pairs(Cache) do insert(Result, V) end return Result end function Messages.SetEnabled(InEnabled) Enabled = InEnabled end return Messages ================================================ FILE: Internal/Core/Scale.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local CurrentScale = 1 local Scale = {} function Scale.SetScale(newScale) assert(type(newScale) == "number", "Scale needs to be a number!") CurrentScale = newScale end function Scale.GetScale() return CurrentScale or 1 end function Scale.GetScreenWidth() return love.graphics.getWidth() / Scale.GetScale() end function Scale.GetScreenHeight() return love.graphics.getHeight() / Scale.GetScale() end function Scale.GetScreenDimensions() return Scale.GetScreenWidth(), Scale.GetScreenHeight() end return Scale ================================================ FILE: Internal/Core/Stats.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Stats = {} local insert = table.insert local max = math.max local Data = {} local Pending = {} local Enabled = false local QueueEnabled = false local QueueDisable = false local Id = 1 local QueueFlush = false local FrameNumber = 0 local function GetCategory(Category) assert(Category ~= nil, "Nil category given to Stats system.") assert(Category ~= '', "Empty category given to Stats system.") assert(type(Category) == 'string', "Category given is not of type string. Type given is '" .. type(Category) .. "'.") if Data[Category] == nil then Data[Category] = {} end return Data[Category] end local function ResetCategory(Category) local Instance = Data[Category] if Instance ~= nil then for K, V in pairs(Instance) do V.LastTime = V.Time V.LastCallCount = V.CallCount V.MaxTime = max(V.MaxTime, V.Time) V.Time = 0.0 V.CallCount = 0 end end end local function GetItem(Name, Category) assert(Name ~= nil, "Nil name given to Stats system.") assert(Name ~= '', "Empty name given to Stats system.") local Cat = GetCategory(Category) if Cat[Name] == nil then local Instance = {} Instance.Time = 0.0 Instance.MaxTime = 0.0 Instance.CallCount = 0 Instance.LastTime = 0.0 Instance.LastCallCount = 0.0 Cat[Name] = Instance end return Cat[Name] end function Stats.Begin(Name, Category) if not Enabled then return end local Handle = Id Id = Id + 1 local Instance = {StartTime = love.timer.getTime(), Name = Name, Category = Category} Pending[Handle] = Instance return Handle end function Stats.End(Handle) if not Enabled then return end assert(Handle ~= nil, "Nil handle given to Stats.End.") local Instance = Pending[Handle] assert(Instance ~= nil, "Invalid handle given to Stats.End.") Pending[Handle] = nil local Elapsed = love.timer.getTime() - Instance.StartTime local Item = GetItem(Instance.Name, Instance.Category) Item.CallCount = Item.CallCount + 1 Item.Time = Item.Time + Elapsed end function Stats.GetTime(Name, Category) if not Enabled then return 0.0 end local Item = GetItem(Name, Category) return Item.Time > 0.0 and Item.Time or Item.LastTime end function Stats.GetMaxTime(Name, Category) if not Enabled then return 0.0 end local Item = GetItem(Name, Category) return Item.MaxTime end function Stats.GetCallCount(Name, Category) if not Enabled then return 0 end local Item = GetItem(Name, Category) return Item.CallCount > 0 and Item.CallCount or Item.LastCallCount end function Stats.Reset(Strict) FrameNumber = FrameNumber + 1 if QueueEnabled then Enabled = true QueueEnabled = false end if QueueDisable then Enabled = false QueueDisable = false end if QueueFlush then Data = {} Pending = {} Id = 1 QueueFlush = false end if not Enabled then return end local Message = nil for K, V in pairs(Pending) do if Message == nil then Message = "Stats.End were not called for the given stats: \n" end Message = Message .. "\t" .. tostring(V.Name) .. " in " .. tostring(V.Category) .. "\n" end if Strict then assert(Message == nil, Message) end for K, V in pairs(Data) do ResetCategory(K) end end function Stats.SetEnabled(IsEnabled) QueueEnabled = IsEnabled if not QueueEnabled then QueueDisable = true end end function Stats.IsEnabled() return Enabled end function Stats.GetCategories() local Result = {} for K, V in pairs(Data) do insert(Result, K) end return Result end function Stats.GetItems(Category) local Result = {} local Instance = GetCategory(Category) for K, V in pairs(Instance) do insert(Result, K) end return Result end function Stats.Flush() QueueFlush = true end function Stats.GetFrameNumber() return FrameNumber end return Stats ================================================ FILE: Internal/Core/TablePool.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local TablePool = {} TablePool.__index = TablePool function TablePool:pull() local count = self[0] if count == 0 then return {} end local result = self[count] self[count], self[0] = nil, count - 1 return result end function TablePool:pullClean() local result = self:pull() for k in pairs(result) do result[k] = nil end return result end function TablePool:push(t) local count = self[0] + 1 self[count], self[0] = t, count end return function() return setmetatable({ [0] = 0 }, TablePool) end ================================================ FILE: Internal/Core/Utility.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Utility = {} local abs = math.abs local remove = table.remove function Utility.MakeColor(color, target) target = target or {} if color then target[1] = color[1] target[2] = color[2] target[3] = color[3] target[4] = color[4] else target[1] = 0 target[2] = 0 target[3] = 0 target[4] = 1 end return target end function Utility.HSVtoRGB(H, S, V) if S == 0.0 then return V, V, V end H = math.fmod(H, 1.0) / (60.0/360.0) local I = math.floor(H) local F = H - I local P = V * (1.0 - S) local Q = V * (1.0 - S * F) local T = V * (1.0 - S * (1.0 - F)) local R, G, B = 0, 0, 0 if I == 0 then R, G, B = V, T, P elseif I == 1 then R, G, B = Q, V, P elseif I == 2 then R, G, B = P, V, T elseif I == 3 then R, G, B = P, Q, V elseif I == 4 then R, G, B = T, P, V else R, G, B = V, P, Q end return R, G, B end function Utility.RGBtoHSV(R, G, B) local K = 0.0 if G < B then local T = G G = B B = T K = -1.0 end if R < G then local T = R R = G G = T K = -2.0 / 6.0 - K end local Chroma = R - (G < B and G or B) local H = abs(K + (G - B) / (6.0 * Chroma + 1e-20)) local S = Chroma / (R + 1e-20) local V = R return H, S, V end function Utility.HasValue(Table, Value) for I, V in ipairs(Table) do if V == Value then return true end end return false end function Utility.Remove(Table, Value) for I, V in ipairs(Table) do if V == Value then remove(Table, I) break end end end function Utility.CopyValues(A, B) if type(A) ~= "table" or type(B) ~= "table" then return end for K, V in pairs(A, B) do local Other = B[K] if Other ~= nil then A[K] = Utility.Copy(Other) end end end function Utility.Copy(Original) local Copy = nil if type(Original) == "table" then Copy = {} for K, V in next, Original, nil do Copy[Utility.Copy(K)] = Utility.Copy(V) end else Copy = Original end return Copy end function Utility.Contains(Table, Value) if Table == nil then return false end for I, V in ipairs(Table) do if Value == V then return true end end return false end function Utility.TableCount(Table) local Result = 0 if Table ~= nil then for K, V in pairs(Table) do Result = Result + 1 end end return Result end local OS = love.system.getOS() function Utility.IsWindows() return OS == "Windows" end function Utility.IsOSX() return OS == "OS X" end function Utility.IsMobile() return (OS == "Android") or (OS == "iOS") end function Utility.Clamp(Value, Min, Max) return Value < Min and Min or (Value > Max and Max or Value) end return Utility ================================================ FILE: Internal/Input/Common.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Common = {} Common.Event = { None = 0, Pressed = 1, Released = 2 } return Common ================================================ FILE: Internal/Input/Keyboard.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local Common = require(SLAB_PATH .. '.Internal.Input.Common') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Keyboard = {} local KeyPressedFn = nil local KeyReleasedFn = nil local Events = {} local Keys = {} local function PushEvent(Type, Key, Scancode, IsRepeat) insert(Events, { Type = Type, Key = Key, Scancode = Scancode, IsRepeat = IsRepeat, Frame = Stats.GetFrameNumber() }) end local function OnKeyPressed(Key, Scancode, IsRepeat) PushEvent(Common.Event.Pressed, Key, Scancode, IsRepeat) if KeyPressedFn ~= nil then KeyPressedFn(Key, Scancode, IsRepeat) end end local function OnKeyReleased(Key, Scancode) PushEvent(Common.Event.Released, Key, Scancode, false) if KeyReleasedFn ~= nil then KeyReleasedFn(Key, Scancode) end end local function ProcessEvents() Keys = {} -- Soft keyboards found on mobile/tablet devices will push keypressed/keyreleased events when the user -- releases from the pressed key. All released events pushed as the same frame as the pressed events will be -- pushed to the events table for the next frame to process. local NextEvents = {} for I, V in ipairs(Events) do if Keys[V.Scancode] == nil then Keys[V.Scancode] = {} end local Key = Keys[V.Scancode] if Utility.IsMobile() and V.Type == Common.Event.Released and Key.Frame == V.Frame then V.Frame = V.Frame + 1 insert(NextEvents, V) else Key.Type = V.Type Key.Key = V.Key Key.Scancode = V.Scancode Key.IsRepeat = V.IsRepeat Key.Frame = V.Frame end end Events = NextEvents end Keyboard.OnKeyPressed = OnKeyPressed; Keyboard.OnKeyReleased = OnKeyReleased; function Keyboard.Initialize(Args, dontInterceptEventHandlers) if not dontInterceptEventHandlers then KeyPressedFn = love.handlers['keypressed'] KeyReleasedFn = love.handlers['keyreleased'] love.handlers['keypressed'] = OnKeyPressed love.handlers['keyreleased'] = OnKeyReleased end end function Keyboard.Update() ProcessEvents() end function Keyboard.IsPressed(Key) local Item = Keys[Key] if Item == nil then return false end return Item.Type == Common.Event.Pressed end function Keyboard.IsReleased(Key) local Item = Keys[Key] if Item == nil then return false end return Item.Type == Common.Event.Released end function Keyboard.IsDown(Key) return love.keyboard.isScancodeDown(Key) end return Keyboard ================================================ FILE: Internal/Input/Mouse.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local Common = require(SLAB_PATH .. '.Internal.Input.Common') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local TablePool = require(SLAB_PATH .. '.Internal.Core.TablePool') local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local Utility = require(SLAB_PATH .. ".Internal.Core.Utility") local Mouse = {} local State = { X = 0.0, Y = 0.0, DeltaX = 0.0, DeltaY = 0.0, AsyncDeltaX = 0.0, AsyncDeltaY = 0.0, Buttons = {} } local Cursors = nil local CurrentCursor = "arrow" local PendingCursor = "" local MouseMovedFn = nil local MousePressedFn = nil local MouseReleasedFn = nil local Events = {} local eventPool = TablePool() -- Custom cursors allow the developer to override any specific system cursor used. This system will also -- allow developers to set an empty image to hide the cursor for specific states, such as mouse resize. -- For more information, refer to the SetCustomCursor/ClearCustomCursor functions. local CustomCursors = {} local function ScaleMouseXY(X, Y) local scale = Scale.GetScale() return X / scale, Y / scale end local function TransformPoint(X,Y) return ScaleMouseXY(X, Y) end local function OnMouseMoved(X, Y, DX, DY, IsTouch) local tX, tY = TransformPoint(X, Y) State.X = tX State.Y = tY State.AsyncDeltaX = State.AsyncDeltaX + DX State.AsyncDeltaY = State.AsyncDeltaY + DY if MouseMovedFn ~= nil then MouseMovedFn(X, Y, DX, DY, IsTouch) end end local function PushEvent(Type, X, Y, Button, IsTouch, Presses) local ev = eventPool:pull() ev.Type = Type ev.X = X ev.Y = Y ev.Button = Button ev.IsTouch = IsTouch ev.Presses = Presses insert(Events, 1, ev) end local function OnMousePressed(X, Y, Button, IsTouch, Presses) local tX, tY = TransformPoint(X, Y) PushEvent(Common.Event.Pressed, tX, tY, Button, IsTouch, Presses) if MousePressedFn ~= nil then MousePressedFn(X, Y, Button, IsTouch, Presses) end end local function OnMouseReleased(X, Y, Button, IsTouch, Presses) local tX, tY = TransformPoint(X, Y) PushEvent(Common.Event.Released, tX, tY, Button, IsTouch, Presses) if MouseReleasedFn ~= nil then MouseReleasedFn(X, Y, Button, IsTouch, Presses) end end local function ProcessEvents() for k, v in pairs(State.Buttons) do v.Type = Common.Event.None end local wasPressed = false for i = #Events, 1, -1 do local ev = Events[i] -- delay release events until next frame if ev.Type == Common.Event.Released and wasPressed then break end wasPressed = ev.Type == Common.Event.Pressed if State.Buttons[ev.Button] == nil then State.Buttons[ev.Button] = {} end local button = State.Buttons[ev.Button] button.Type = ev.Type button.IsTouch = ev.IsTouch button.Presses = ev.Presses eventPool:push(Events[i]) Events[i] = nil end end Mouse.OnMouseMoved = OnMouseMoved; Mouse.OnMousePressed = OnMousePressed; Mouse.OnMouseReleased = OnMouseReleased; function Mouse.Initialize(Args, dontInterceptEventHandlers) TransformPoint = Args.TransformPointToSlab or TransformPoint if not dontInterceptEventHandlers then MouseMovedFn = love.handlers['mousemoved'] MousePressedFn = love.handlers['mousepressed'] MouseReleasedFn = love.handlers['mousereleased'] love.handlers['mousemoved'] = OnMouseMoved love.handlers['mousepressed'] = OnMousePressed love.handlers['mousereleased'] = OnMouseReleased end end function Mouse.Update() ProcessEvents() State.DeltaX = State.AsyncDeltaX State.DeltaY = State.AsyncDeltaY State.AsyncDeltaX = 0 State.AsyncDeltaY = 0 if Cursors == nil and (not Utility.IsMobile()) then Cursors = {} Cursors.Arrow = love.mouse.getSystemCursor('arrow') Cursors.SizeWE = love.mouse.getSystemCursor('sizewe') Cursors.SizeNS = love.mouse.getSystemCursor('sizens') Cursors.SizeNESW = love.mouse.getSystemCursor('sizenesw') Cursors.SizeNWSE = love.mouse.getSystemCursor('sizenwse') Cursors.IBeam = love.mouse.getSystemCursor('ibeam') Cursors.Hand = love.mouse.getSystemCursor('hand') end Mouse.SetCursor('arrow') end function Mouse.Draw() Mouse.UpdateCursor() local CustomCursor = CustomCursors[CurrentCursor] if CustomCursor ~= nil then DrawCommands.SetLayer('Mouse') DrawCommands.Begin() if CustomCursor.Quad ~= nil then local X, Y, W, H = CustomCursor.Quad:getViewport() DrawCommands.SubImage(State.X, State.Y, CustomCursor.Image, X, Y, W, H) else DrawCommands.Image(State.X, State.Y, CustomCursor.Image) end DrawCommands.End() end end function Mouse.IsDown(Button) return love.mouse.isDown(Button) end function Mouse.IsClicked(Button) local Item = State.Buttons[Button] if Item == nil or Item.Presses == 0 then return false end return Item.Type == Common.Event.Pressed end function Mouse.IsDoubleClicked(Button) local Item = State.Buttons[Button] if Item == nil or Item.Presses < 2 then return false end return Item.Type == Common.Event.Pressed and Item.Presses % 2 == 0 end function Mouse.IsReleased(Button) local Item = State.Buttons[Button] if Item == nil then return false end return Item.Type == Common.Event.Released end function Mouse.Position() return State.X, State.Y end function Mouse.HasDelta() return State.DeltaX ~= 0.0 or State.DeltaY ~= 0.0 end function Mouse.GetDelta() return State.DeltaX, State.DeltaY end function Mouse.IsDragging(Button) return Mouse.IsDown(Button) and Mouse.HasDelta() end function Mouse.SetCursor(Type) if Cursors == nil then return end PendingCursor = Type end function Mouse.UpdateCursor() if PendingCursor ~= "" and PendingCursor ~= CurrentCursor then CurrentCursor = PendingCursor PendingCursor = "" if CustomCursors[CurrentCursor] ~= nil then love.mouse.setVisible(false) else love.mouse.setVisible(true) local Type = CurrentCursor if Type == 'arrow' then love.mouse.setCursor(Cursors.Arrow) elseif Type == 'sizewe' then love.mouse.setCursor(Cursors.SizeWE) elseif Type == 'sizens' then love.mouse.setCursor(Cursors.SizeNS) elseif Type == 'sizenesw' then love.mouse.setCursor(Cursors.SizeNESW) elseif Type == 'sizenwse' then love.mouse.setCursor(Cursors.SizeNWSE) elseif Type == 'ibeam' then love.mouse.setCursor(Cursors.IBeam) elseif Type == 'hand' then love.mouse.setCursor(Cursors.Hand) end end end end function Mouse.SetCustomCursor(Type, Image, Quad) -- If no image is supplied, then create a 1x1 image with no alpha. This is a way to disable certain system cursors. if Image == nil then local Data = love.image.newImageData(1, 1) Image = love.graphics.newImage(Data) end if CustomCursors[Type] == nil then CustomCursors[Type] = {} end CustomCursors[Type].Image = Image CustomCursors[Type].Quad = Quad PendingCursor = CurrentCursor CurrentCursor = "" end function Mouse.ClearCustomCursor(Type) CustomCursors[Type] = nil PendingCursor = CurrentCursor CurrentCursor = "" end return Mouse ================================================ FILE: Internal/Resources/Styles/Dark.style ================================================ FontSize = 14 MenuColor = (0.2, 0.2, 0.2, 1.0) ScrollBarColor = (0.4, 0.4, 0.4, 1.0) ScrollBarHoveredColor = (0.8, 0.8, 0.8, 1.0) SeparatorColor = (0.5, 0.5, 0.5, 0.7) WindowBackgroundColor = (0.2, 0.2, 0.2, 1.0) WindowTitleFocusedColor = (0.26, 0.53, 0.96, 1.0) WindowCloseBgColor = (0.64, 0.64, 0.64, 1.0) WindowCloseColor = (0.0, 0.0, 0.0, 1.0) ButtonColor = (0.55, 0.55, 0.55, 1.0) RadioButtonSelectedColor = (0.2, 0.2, 0.2, 1.0) ButtonHoveredColor = (0.7, 0.7, 0.7, 1.0) ButtonPressedColor = (0.8, 0.8, 0.8, 1.0) ButtonDisabledTextColor = (0.35, 0.35, 0.35, 1.0) CheckBoxSelectedColor = (0.0, 0.0, 0.0, 1.0) CheckBoxDisabledColor = (0.35, 0.35, 0.35, 1.0) TextColor = (0.875, 0.875, 0.875, 1.0) TextDisabledColor = (0.45, 0.45, 0.45, 1.0) TextHoverBgColor = (0.5, 0.5, 0.5, 1.0) TextURLColor = (0.2, 0.2, 1.0, 1.0) ComboBoxColor = (0.4, 0.4, 0.4, 1.0) ComboBoxHoveredColor = (0.55, 0.55, 0.55, 1.0) ComboBoxDropDownColor = (0.4, 0.4, 0.4, 1.0) ComboBoxDropDownHoveredColor = (0.55, 0.55, 0.55, 1.0) ComboBoxArrowColor = (1.0, 1.0, 1.0, 1.0) InputBgColor = (0.4, 0.4, 0.4, 1.0) InputEditBgColor = (0.6, 0.6, 0.6, 1.0) InputSelectColor = (0.14, 0.29, 0.53, 0.4) InputSliderColor = (0.1, 0.1, 0.1, 1.0) MultilineTextColor = (0.0, 0.0, 0.0, 1.0) ListBoxBgColor = (0.0, 0.0, 0.0, 0.0) WindowRounding = 2.0 WindowBorder = 4.0 WindowTitleH = 0.0 ButtonRounding = 2.0 CheckBoxRounding = 2.0 ComboBoxRounding = 2.0 InputBgRounding = 2.0 ScrollBarRounding = 2.0 Indent = 14.0 MenuPadH = 0.0 MenuItemPadH = 0.0 ================================================ FILE: Internal/Resources/Styles/Light.style ================================================ FontSize = 14 MenuColor = (0.74, 0.74, 0.94, 1.0) ScrollBarColor = (0.4, 0.62, 0.80, 0.3) ScrollBarHoveredColor = (0.28, 0.67, 0.8, 0.59) SeparatorColor = (0.5, 0.5, 0.5, 0.7) WindowBackgroundColor = (0.94, 0.94, 0.94, 1.0) WindowTitleFocusedColor = (0.42, 0.75, 1.0, 1.0) WindowCloseBgColor = (0.64, 0.64, 0.64, 1.0) WindowCloseColor = (0.0, 0.0, 0.0, 1.0) ButtonColor = (1.0, 0.79, 0.18, 0.78) RadioButtonSelectedColor = (0.31, 0.25, 0.24, 1.0) ButtonHoveredColor = (0.42, 0.82, 1.0, 0.81) ButtonPressedColor = (0.72, 1.0, 1.0, 0.86) ButtonDisabledTextColor = (0.55, 0.55, 0.55, 1.0) CheckBoxSelectedColor = (0.31, 0.25, 0.24, 1.0) CheckBoxDisabledColor = (0.55, 0.55, 0.55, 1.0) TextColor = (0.31, 0.25, 0.24, 1.0) TextDisabledColor = (0.55, 0.55, 0.55, 1.0) TextHoverBgColor = (1.0, 0.99, 0.54, 0.43) TextURLColor = (0.2, 0.2, 1.0, 1.0) ComboBoxColor = (0.89, 0.98, 1.0, 1.0) ComboBoxHoveredColor = (0.14, 0.29, 0.53, 0.4) ComboBoxDropDownColor = (0.89, 0.98, 1.0, 1.0) ComboBoxDropDownHoveredColor = (0.14, 0.29, 0.53, 0.4) ComboBoxArrowColor = (0.31, 0.25, 0.24, 1.0) InputBgColor = (0.89, 0.98, 1.0, 1.0) InputEditBgColor = (0.89, 0.98, 1.0, 1.0) InputSelectColor = (0.14, 0.29, 0.53, 0.4) InputSliderColor = (1.0, 0.79, 0.18, 1.0) MultilineTextColor = (0.0, 0.0, 0.0, 1.0) ListBoxBgColor = (0.0, 0.0, 0.0, 0.0) WindowRounding = 2.0 WindowBorder = 4.0 WindowTitleH = 0.0 ButtonRounding = 2.0 CheckBoxRounding = 2.0 ComboBoxRounding = 2.0 InputBgRounding = 2.0 ScrollBarRounding = 2.0 Indent = 14.0 MenuPadH = 0.0 MenuItemPadH = 0.0 ================================================ FILE: Internal/UI/Button.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local floor = math.floor local max = math.max local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local Image = require(SLAB_PATH .. '.Internal.UI.Image') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Tooltip = require(SLAB_PATH .. '.Internal.UI.Tooltip') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Button = {} local PAD = 10.0 local MINWIDTH = 75.0 local RADIUS = 8.0 local EMPTY = {} local IGNORE = { Ignore = true } local clickedId = nil local labelColor = {} function Button.Begin(label, options) local statHandle = Stats.Begin('Button', 'Slab') options = options or EMPTY local width, height = options.W, options.H local disabled = options.Disabled local image = options.Image local color = options.Color or Style.ButtonColor local hoverColor = options.HoverColor or Style.ButtonHoveredColor local pressColor = options.PressColor or Style.ButtonPressedColor local padX = options.PadX or PAD * 2.0 local padY = options.PadY or PAD * 0.5 local vLines = options.VLines or 1 if options.Active then color = pressColor end local id = Window.GetItemId(label) local w, h = Button.GetSize(label) h = h * vLines -- If a valid image was specified, then adjust the button size to match the requested image size. Also takes into account any sub UVs. local imageW, imageH = w, h if image ~= nil then imageW, imageH = Image.GetSize(image.Image or image.Path) imageW = image.SubW or imageW imageH = image.SubH or imageH imageW = width or imageW imageH = height or imageH image.W = imageW image.H = imageH if imageW > 0 and imageH > 0 then w = imageW + padX h = imageH + padY end end w, h = LayoutManager.ComputeSize(width or w, height or h) LayoutManager.AddControl(w, h, 'Button') local x, y = Cursor.GetPosition() local result = false do local mouseX, mouseY = Window.GetMousePosition() if not Window.IsObstructedAtMouse() and x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then Tooltip.Begin(options.Tooltip or "") Window.SetHotItem(id) if not disabled then if not Utility.IsMobile() then color = hoverColor end if clickedId == id then color = pressColor end if Mouse.IsClicked(1) then clickedId = id end if Mouse.IsReleased(1) and clickedId == id then result = true clickedId = nil end end end end if not options.Invisible then -- Draw the background. DrawCommands.Rectangle('fill', x, y, w, h, color, options.Rounding or Style.ButtonRounding) -- Draw the label or image. The layout of this control was already computed above. Ignore when adding sub-controls -- such as text or an image. local cursorX, cursorY = Cursor.GetPosition() LayoutManager.Begin('Ignore', IGNORE) if image ~= nil then Cursor.SetX(x + w * 0.5 - imageW * 0.5) Cursor.SetY(y + h * 0.5 - imageH * 0.5) Image.Begin(id .. '_Image', image) else local labelX = x + (w * 0.5) - (Style.Font:getWidth(label) * 0.5) local fontHeight = Style.Font:getHeight() * vLines Cursor.SetX(floor(labelX)) Cursor.SetY(floor(y + (h * 0.5) - (fontHeight * 0.5))) labelColor.color = disabled and Style.ButtonDisabledTextColor or nil Text.Begin(label, labelColor) end LayoutManager.End() Cursor.SetPosition(cursorX, cursorY) end Cursor.SetItemBounds(x, y, w, h) Cursor.AdvanceY(h) Window.AddItem(x, y, w, h, id) Stats.End(statHandle) return result end function Button.BeginRadio(label, options) local statHandle = Stats.Begin('RadioButton', 'Slab') label = label or "" options = options or EMPTY local index = options.Index or 0 local selectedIndex = options.SelectedIndex or 0 local result = false local id = Window.GetItemId(label) local w, h = RADIUS * 2.0, RADIUS * 2.0 local isObstructed = Window.IsObstructedAtMouse() local color = Style.ButtonColor local mouseX, mouseY = Window.GetMousePosition() if label ~= "" then local TextW, TextH = Text.GetSize(label) w = w + Cursor.PadX() + TextW h = max(h, TextH) end LayoutManager.AddControl(w, h, 'Radio') local x, y = Cursor.GetPosition() local centerX, centerY = x + RADIUS, y + RADIUS local dx = mouseX - centerX local dy = mouseY - centerY if not isObstructed and (dx * dx) + (dy * dy) <= RADIUS * RADIUS then color = Style.ButtonHoveredColor if clickedId == id then color = Style.ButtonPressedColor end if Mouse.IsClicked(1) then clickedId = id end if Mouse.IsReleased(1) and clickedId == id then result = true clickedId = nil end end DrawCommands.Circle('fill', centerX, centerY, RADIUS, color) if index > 0 and index == selectedIndex then DrawCommands.Circle('fill', centerX, centerY, RADIUS * 0.7, Style.RadioButtonSelectedColor) end if label ~= "" then local cursorY = Cursor.GetY() Cursor.AdvanceX(RADIUS * 2.0) LayoutManager.Begin('Ignore', IGNORE) Text.Begin(label) LayoutManager.End() Cursor.SetY(cursorY) end if not isObstructed and x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then Tooltip.Begin(options.Tooltip or "") Window.SetHotItem(id) end Cursor.SetItemBounds(x, y, w, h) Cursor.AdvanceY(h) Window.AddItem(x, y, w, h) Stats.End(statHandle) return result end function Button.GetSize(label) local w = Style.Font:getWidth(label) local h = Style.Font:getHeight() return max(w, MINWIDTH) + PAD * 2.0, h + PAD * 0.5 end function Button.ClearClicked() clickedId = nil end return Button ================================================ FILE: Internal/UI/CheckBox.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local max = math.max local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Tooltip = require(SLAB_PATH .. '.Internal.UI.Tooltip') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local CheckBox = {} local EMPTY = {} local IGNORE = { Ignore = true } local labelColor = {} function CheckBox.Begin(checked, label, options) local statHandle = Stats.Begin('CheckBox', 'Slab') label = label or "" options = options or EMPTY local id = options.Id or label local rounding = options.Rounding or Style.CheckBoxRounding local size = options.Size or Style.Font:getHeight() local disabled = options.Disabled local itemId = Window.GetItemId(id) local boxW, boxH = size, size local textW, textH = Text.GetSize(label) local w = boxW + Cursor.PadX() + 2.0 + textW local h = max(boxH, textH) local radius = size * 0.5 LayoutManager.AddControl(w, h, 'CheckBox') local result = false local color = disabled and Style.CheckBoxDisabledColor or Style.ButtonColor local x, y = Cursor.GetPosition() local mouseX, mouseY = Window.GetMousePosition() local isObstructed = Window.IsObstructedAtMouse() if not isObstructed and not disabled and x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then color = Style.ButtonHoveredColor if Mouse.IsDown(1) then color = Style.ButtonPressedColor elseif Mouse.IsReleased(1) then result = true end end DrawCommands.Rectangle('fill', x, y, boxW, boxH, color, rounding) if checked then DrawCommands.Cross(x + radius, y + radius, radius - 1.0, Style.CheckBoxSelectedColor) end if label ~= "" then local cursorY = Cursor.GetY() Cursor.AdvanceX(boxW + 2.0) LayoutManager.Begin('Ignore', IGNORE) labelColor.Color = disabled and Style.TextDisabledColor or nil Text.Begin(label, labelColor) LayoutManager.End() Cursor.SetY(cursorY) end if not isObstructed and x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then Tooltip.Begin(options.Tooltip or "") Window.SetHotItem(itemId) end Cursor.SetItemBounds(x, y, w, h) Cursor.AdvanceY(h) Window.AddItem(x, y, w, h, itemId) Stats.End(statHandle) return result end return CheckBox ================================================ FILE: Internal/UI/ColorPicker.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local ceil = math.ceil local max = math.max local min = math.min local insert = table.insert local unpack = table.unpack local Button = require(SLAB_PATH .. '.Internal.UI.Button') local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local Image = require(SLAB_PATH .. '.Internal.UI.Image') local Input = require(SLAB_PATH .. '.Internal.UI.Input') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local ColorPicker = {} local SaturationMeshes = nil local SaturationSize = 200.0 local SaturationStep = 5 local SaturationFocused = false local TintMeshes = nil local TintW = 30.0 local TintH = SaturationSize local TintFocused = false local AlphaMesh = nil local AlphaW = TintW local AlphaH = TintH local AlphaFocused = false local CurrentColor = {1.0, 1.0, 1.0, 1.0} local ColorH = 25.0 local function IsEqual(A, B) for I, V in ipairs(A) do if V ~= B[I] then return false end end return true end local function InputColor(Component, Value, OffsetX) local Changed = false Text.Begin(string.format("%s ", Component)) Cursor.SameLine() Cursor.SetRelativeX(OffsetX) if Input.Begin('ColorPicker_' .. Component, {W = 40.0, NumbersOnly = true, Text = tostring(ceil(Value * 255)), ReturnOnText = false}) then local NewValue = tonumber(Input.GetText()) if NewValue ~= nil then NewValue = max(NewValue, 0) NewValue = min(NewValue, 255) Value = NewValue / 255 Changed = true end end return Value, Changed end local function UpdateSaturationColors() if SaturationMeshes ~= nil then local MeshIndex = 1 local Step = SaturationStep local C00 = {1.0, 1.0, 1.0, 1.0} local C10 = {1.0, 1.0, 1.0, 1.0} local C01 = {1.0, 1.0, 1.0, 1.0} local C11 = {1.0, 1.0, 1.0, 1.0} local StepX, StepY = 0, 0 local Hue, Sat, Val = Utility.RGBtoHSV(CurrentColor[1], CurrentColor[2], CurrentColor[3]) for I = 1, Step, 1 do for J = 1, Step, 1 do local S0 = StepX / Step local S1 = (StepX + 1) / Step local V0 = 1.0 - (StepY / Step) local V1 = 1.0 - ((StepY + 1) / Step) C00[1], C00[2], C00[3] = Utility.HSVtoRGB(Hue, S0, V0) C10[1], C10[2], C10[3] = Utility.HSVtoRGB(Hue, S1, V0) C01[1], C01[2], C01[3] = Utility.HSVtoRGB(Hue, S0, V1) C11[1], C11[2], C11[3] = Utility.HSVtoRGB(Hue, S1, V1) local Mesh = SaturationMeshes[MeshIndex] MeshIndex = MeshIndex + 1 Mesh:setVertexAttribute(1, 3, C00[1], C00[2], C00[3], C00[4]) Mesh:setVertexAttribute(2, 3, C10[1], C10[2], C10[3], C10[4]) Mesh:setVertexAttribute(3, 3, C11[1], C11[2], C11[3], C11[4]) Mesh:setVertexAttribute(4, 3, C01[1], C01[2], C01[3], C01[4]) StepX = StepX + 1 end StepX = 0 StepY = StepY + 1 end end end local function InitializeSaturationMeshes() if SaturationMeshes == nil then SaturationMeshes = {} local Step = SaturationStep local X, Y = 0.0, 0.0 local Size = SaturationSize / Step for I = 1, Step, 1 do for J = 1, Step, 1 do local Verts = { { X, Y, 0.0, 0.0 }, { X + Size, Y, 1.0, 0.0 }, { X + Size, Y + Size, 1.0, 1.0 }, { X, Y + Size, 0.0, 1.0 } } local NewMesh = love.graphics.newMesh(Verts) insert(SaturationMeshes, NewMesh) X = X + Size end X = 0.0 Y = Y + Size end end UpdateSaturationColors() end local function InitializeTintMeshes() if TintMeshes == nil then TintMeshes = {} local Step = 6 local X, Y = 0.0, 0.0 local C0 = {1.0, 1.0, 1.0, 1.0} local C1 = {1.0, 1.0, 1.0, 1.0} local I = 0 local Colors = { {1.0, 0.0, 0.0, 1.0}, {1.0, 1.0, 0.0, 1.0}, {0.0, 1.0, 0.0, 1.0}, {0.0, 1.0, 1.0, 1.0}, {0.0, 0.0, 1.0, 1.0}, {1.0, 0.0, 1.0, 1.0}, {1.0, 0.0, 0.0, 1.0} } for Index = 1, Step, 1 do C0 = Colors[Index] C1 = Colors[Index + 1] local Verts = { { X, Y, 0.0, 0.0, C0[1], C0[2], C0[3], C0[4] }, { TintW, Y, 1.0, 0.0, C0[1], C0[2], C0[3], C0[4] }, { TintW, Y + TintH / Step, 1.0, 1.0, C1[1], C1[2], C1[3], C1[4] }, { X, Y + TintH / Step, 0.0, 1.0, C1[1], C1[2], C1[3], C1[4] } } local NewMesh = love.graphics.newMesh(Verts) insert(TintMeshes, NewMesh) Y = Y + TintH / Step end end end local function InitializeAlphaMesh() if AlphaMesh == nil then local Verts = { { 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0 }, { AlphaW, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0 }, { AlphaW, AlphaH, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0 }, { 0.0, AlphaH, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 } } AlphaMesh = love.graphics.newMesh(Verts) end end function ColorPicker.Begin(Options) Options = Options == nil and {} or Options Options.Color = Options.Color == nil and {1.0, 1.0, 1.0, 1.0} or Options.Color Options.Refresh = Options.Refresh == nil and false or Options.Refresh Options.X = Options.X == nil and nil or Options.X Options.Y = Options.Y == nil and nil or Options.Y if SaturationMeshes == nil then InitializeSaturationMeshes() end if TintMeshes == nil then InitializeTintMeshes() end if AlphaMesh == nil then InitializeAlphaMesh() end Window.Begin('ColorPicker', {Title = "Color Picker", X = Options.X, Y = Options.Y}) if Window.IsAppearing() or Options.Refresh then CurrentColor[1] = Options.Color[1] or 0.0 CurrentColor[2] = Options.Color[2] or 0.0 CurrentColor[3] = Options.Color[3] or 0.0 CurrentColor[4] = Options.Color[4] or 1.0 UpdateSaturationColors() end local X, Y = Cursor.GetPosition() local MouseX, MouseY = Window.GetMousePosition() local H, S, V = Utility.RGBtoHSV(CurrentColor[1], CurrentColor[2], CurrentColor[3]) local UpdateColor = false local MouseClicked = Mouse.IsClicked(1) and not Window.IsObstructedAtMouse() if SaturationMeshes ~= nil then for I, V in ipairs(SaturationMeshes) do DrawCommands.Mesh(V, X, Y) end Window.AddItem(X, Y, SaturationSize, SaturationSize) local UpdateSaturation = false if X <= MouseX and MouseX < X + SaturationSize and Y <= MouseY and MouseY < Y + SaturationSize then if MouseClicked then SaturationFocused = true UpdateSaturation = true end end if SaturationFocused and Mouse.IsDragging(1) then UpdateSaturation = true end if UpdateSaturation then local CanvasX = max(MouseX - X, 0) CanvasX = min(CanvasX, SaturationSize) local CanvasY = max(MouseY - Y, 0) CanvasY = min(CanvasY, SaturationSize) S = CanvasX / SaturationSize V = 1 - (CanvasY / SaturationSize) UpdateColor = true end local SaturationX = S * SaturationSize local SaturationY = (1.0 - V) * SaturationSize DrawCommands.Circle('line', X + SaturationX, Y + SaturationY, 4.0, {1.0, 1.0, 1.0, 1.0}) X = X + SaturationSize + Cursor.PadX() end if TintMeshes ~= nil then for I, V in ipairs(TintMeshes) do DrawCommands.Mesh(V, X, Y) end Window.AddItem(X, Y, TintW, TintH) local UpdateTint = false if X <= MouseX and MouseX < X + TintW and Y <= MouseY and MouseY < Y + TintH then if MouseClicked then TintFocused = true UpdateTint = true end end if TintFocused and Mouse.IsDragging(1) then UpdateTint = true end if UpdateTint then local CanvasY = max(MouseY - Y, 0) CanvasY = min(CanvasY, TintH) H = CanvasY / TintH UpdateColor = true end local TintY = H * TintH DrawCommands.Line(X, Y + TintY, X + TintW, Y + TintY, 2.0, {1.0, 1.0, 1.0, 1.0}) X = X + TintW + Cursor.PadX() DrawCommands.Mesh(AlphaMesh, X, Y) Window.AddItem(X, Y, AlphaW, AlphaH) local UpdateAlpha = false if X <= MouseX and MouseX < X + AlphaW and Y <= MouseY and MouseY < Y + AlphaH then if MouseClicked then AlphaFocused = true UpdateAlpha = true end end if AlphaFocused and Mouse.IsDragging(1) then UpdateAlpha = true end if UpdateAlpha then local CanvasY = max(MouseY - Y, 0) CanvasY = min(CanvasY, AlphaH) CurrentColor[4] = 1.0 - CanvasY / AlphaH UpdateColor = true end local A = 1.0 - CurrentColor[4] local AlphaY = A * AlphaH DrawCommands.Line(X, Y + AlphaY, X + AlphaW, Y + AlphaY, 2.0, {A, A, A, 1.0}) Y = Y + AlphaH + Cursor.PadY() end if UpdateColor then CurrentColor[1], CurrentColor[2], CurrentColor[3] = Utility.HSVtoRGB(H, S, V) UpdateSaturationColors() end local OffsetX = Text.GetWidth("##") Cursor.AdvanceY(SaturationSize) X, Y = Cursor.GetPosition() local R = CurrentColor[1] local G = CurrentColor[2] local B = CurrentColor[3] local A = CurrentColor[4] CurrentColor[1], R = InputColor("R", R, OffsetX) CurrentColor[2], G = InputColor("G", G, OffsetX) CurrentColor[3], B = InputColor("B", B, OffsetX) CurrentColor[4], A = InputColor("A", A, OffsetX) if R or G or B or A then UpdateSaturationColors() end local InputX, InputY = Cursor.GetPosition() Cursor.SameLine() X = Cursor.GetX() Cursor.SetY(Y) local WinX, WinY, WinW, WinH = Window.GetBounds() WinW, WinH = Window.GetBorderlessSize() OffsetX = Text.GetWidth("####") local ColorX = X + OffsetX local ColorW = (WinX + WinW) - ColorX Cursor.SetPosition(ColorX, Y) Image.Begin('ColorPicker_CurrentAlpha', { Path = SLAB_FILE_PATH .. "/Internal/Resources/Textures/Transparency.png", SubW = ColorW, SubH = ColorH, WrapH = "repeat", WrapV = "repeat" }) DrawCommands.Rectangle('fill', ColorX, Y, ColorW, ColorH, CurrentColor, Style.ButtonRounding) local LabelW, LabelH = Text.GetSize("New") Cursor.SetPosition(ColorX - LabelW - Cursor.PadX(), Y + (ColorH * 0.5) - (LabelH * 0.5)) Text.Begin("New") Y = Y + ColorH + Cursor.PadY() Cursor.SetPosition(ColorX, Y) Image.Begin('ColorPicker_CurrentAlpha', { Path = SLAB_FILE_PATH .. "/Internal/Resources/Textures/Transparency.png", SubW = ColorW, SubH = ColorH, WrapH = "repeat", WrapV = "repeat" }) DrawCommands.Rectangle('fill', ColorX, Y, ColorW, ColorH, Options.Color, Style.ButtonRounding) local LabelW, LabelH = Text.GetSize("Old") Cursor.SetPosition(ColorX - LabelW - Cursor.PadX(), Y + (ColorH * 0.5) - (LabelH * 0.5)) Text.Begin("Old") if Mouse.IsReleased(1) then SaturationFocused = false TintFocused = false AlphaFocused = false end Cursor.SetPosition(InputX, InputY) Cursor.NewLine() LayoutManager.Begin('ColorPicker_Buttons_Layout', {AlignX = 'right'}) local Result = {Button = 0, Color = Utility.MakeColor(CurrentColor)} if Button.Begin("OK") then Result.Button = 1 end LayoutManager.SameLine() if Button.Begin("Cancel") then Result.Button = -1 Result.Color = Utility.MakeColor(Options.Color) end LayoutManager.End() Window.End() return Result end return ColorPicker ================================================ FILE: Internal/UI/ComboBox.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local min = math.min local max = math.max local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local Input = require(SLAB_PATH .. '.Internal.UI.Input') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Tooltip = require(SLAB_PATH .. '.Internal.UI.Tooltip') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local ComboBox = {} local Instances = {} local Active = nil local MIN_WIDTH = 150.0 local MIN_HEIGHT = 150.0 local EMPTY = {} local IGNORE = { Ignore = true } local inputRounding = { 0, 0, 0, 0 } local dropDownRounding = { 0, 0, 0, 0} local function GetInstance(id) if Instances[id] == nil then local instance = { IsOpen = false, WasOpened = false, WinW = 0.0, WinH = 0.0, StatHandle = nil, InputId = id .. '_Input', WinId = id .. '_combobox', InputOptions = { ReadOnly = true, Align = 'left', Rounding = inputRounding, }, WindowOptions = { AllowResize = false, AutoSizeWindow = false, AllowFocus = false, AutoSizeContent = true, NoSavedSettings = true, } } Instances[id] = instance end return Instances[id] end function ComboBox.Begin(id, options) local StatHandle = Stats.Begin('ComboBox', 'Slab') options = options or EMPTY local w = options.W or MIN_WIDTH local winH = options.WinH or MIN_HEIGHT local selected = options.Selected or "" local rounding = options.Rounding or Style.ComboBoxRounding local instance = GetInstance(id) local winItemId = Window.GetItemId(id) local h = Style.Font:getHeight() w = LayoutManager.ComputeSize(w, h) LayoutManager.AddControl(w, h, 'ComboBox') local x, y = Cursor.GetPosition() local radius = h * 0.35 local inputBgColor = Style.ComboBoxColor local dropDownW = radius * 4.0 local dropDownX = x + w - dropDownW local dropDownColor = Style.ComboBoxDropDownColor inputRounding[1], inputRounding[4] = rounding, rounding dropDownRounding[2], dropDownRounding[3] = rounding, rounding instance.X = x instance.Y = y instance.W = w instance.H = h instance.WinH = min(instance.WinH, winH) instance.StatHandle = StatHandle local mouseX, mouseY = Window.GetMousePosition() instance.WasOpened = instance.IsOpen local hovered = not Window.IsObstructedAtMouse() and x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h if hovered then inputBgColor = Style.ComboBoxHoveredColor dropDownColor = Style.ComboBoxDropDownHoveredColor if Mouse.IsClicked(1) then instance.IsOpen = not instance.IsOpen if instance.IsOpen then Window.SetStackLock(instance.WinId) end end end do LayoutManager.Begin('Ignore', IGNORE) local inputOpts = instance.InputOptions inputOpts.Text = selected inputOpts.W = max(w - dropDownW, dropDownW) inputOpts.H = h inputOpts.BgColor = inputBgColor Input.Begin(instance.InputId, inputOpts) LayoutManager.End() end Cursor.SameLine() DrawCommands.Rectangle('fill', dropDownX, y, dropDownW, h, dropDownColor, dropDownRounding) DrawCommands.Triangle('fill', dropDownX + radius * 2.0, y + h - radius * 1.35, radius, 180, Style.ComboBoxArrowColor) Cursor.SetItemBounds(x, y, w, h) Cursor.AdvanceY(h) if hovered then Tooltip.Begin(options.Tooltip or "") Window.SetHotItem(winItemId) end Window.AddItem(x, y, w, h, winItemId) local winX, winY = Window.TransformPoint(x, y) if instance.IsOpen then LayoutManager.Begin('ComboBox', IGNORE) local winOpts = instance.WindowOptions winOpts.X = winX - 1.0 winOpts.Y = winY + h winOpts.W = max(w, instance.WinW) winOpts.H = instance.WinH winOpts.Layer = Window.GetLayer() winOpts.ContentW = max(w, instance.WinW) winOpts.Border = 4 Window.Begin(instance.WinId, winOpts) Active = instance else Stats.End(instance.StatHandle) end return instance.IsOpen end function ComboBox.End() local y, h = 0, 0 local statHandle = Active and Active.StatHandle or nil if Active ~= nil then Cursor.SetItemBounds(Active.X, Active.Y, Active.W, Active.H) y, h = Active.Y, Active.H local contentW, contentH = Window.GetContentSize() Active.WinH = contentH Active.WinW = max(contentW, Active.W) if Mouse.IsClicked(1) and Active.WasOpened and not Region.IsHoverScrollBar(Window.GetId()) then Active.IsOpen = false Active = nil Window.SetStackLock(nil) end end Window.End() DrawCommands.SetLayer('Normal') LayoutManager.End() if y ~= 0 and h ~= 0 then Cursor.SetY(y) Cursor.AdvanceY(h) end Stats.End(statHandle) end return ComboBox ================================================ FILE: Internal/UI/Dialog.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local remove = table.remove local min = math.min local max = math.max local floor = math.floor local gmatch = string.gmatch local Button = require(SLAB_PATH .. '.Internal.UI.Button') local ComboBox = require(SLAB_PATH .. '.Internal.UI.ComboBox') local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local FileSystem = require(SLAB_PATH .. '.Internal.Core.FileSystem') local Image = require(SLAB_PATH .. '.Internal.UI.Image') local Input = require(SLAB_PATH .. '.Internal.UI.Input') local Keyboard = require(SLAB_PATH .. '.Internal.Input.Keyboard') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local ListBox = require(SLAB_PATH .. '.Internal.UI.ListBox') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Tree = require(SLAB_PATH .. '.Internal.UI.Tree') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local Dialog = {} local Instances = {} local ActiveInstance = nil local Stack = {} local InstanceStack = {} local FileDialog_AskOverwrite = false local FilterW = 0.0 local function ValidateSaveFile(Files, Extension) if Extension == nil or Extension == "" then return end if Files ~= nil and #Files == 1 then local Index = string.find(Files[1], ".", 1, true) if Index ~= nil then Files[1] = string.sub(Files[1], 1, Index - 1) end Files[1] = Files[1] .. Extension end end local function UpdateInputText(Instance) if Instance ~= nil then if #Instance.Return > 0 then Instance.Text = #Instance.Return > 1 and "" or Instance.Return[1] else Instance.Text = "" end end end local function PruneResults(Items, DirectoryOnly) local Result = {} for I, V in ipairs(Items) do if FileSystem.IsDirectory(V) then if DirectoryOnly then insert(Result, V) end else if not DirectoryOnly then insert(Result, V) end end end return Result end local function OpenDirectory(Dir) if ActiveInstance ~= nil and ActiveInstance.Directory ~= nil then ActiveInstance.Parsed = false if string.sub(Dir, #Dir, #Dir) == FileSystem.Separator() then Dir = string.sub(Dir, 1, #Dir - 1) end ActiveInstance.Directory = FileSystem.Sanitize(Dir) end end local function FileDialogItem(Id, Label, IsDirectory, Index) ListBox.BeginItem(Id, {Selected = Utility.HasValue(ActiveInstance.Selected, Index)}) if IsDirectory then local FontH = Style.Font:getHeight() Image.Begin('FileDialog_Folder', {Path = SLAB_FILE_PATH .. "/Internal/Resources/Textures/Icons.png", SubX = 0.0, SubY = 0.0, SubW = 50.0, SubH = 50.0, W = FontH, H = FontH}) Cursor.SameLine({CenterY = true}) end Text.Begin(Label) if ListBox.IsItemClicked(1) then local Set = true if ActiveInstance.AllowMultiSelect then if Keyboard.IsDown('lctrl') or Keyboard.IsDown('rctrl') then Set = false if Utility.HasValue(ActiveInstance.Selected, Index) then Utility.Remove(ActiveInstance.Selected, Index) Utility.Remove(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. Label) else insert(ActiveInstance.Selected, Index) insert(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. Label) end elseif Keyboard.IsDown('lshift') or Keyboard.IsDown('rshift') then if #ActiveInstance.Selected > 0 then Set = false local Anchor = ActiveInstance.Selected[#ActiveInstance.Selected] local Min = min(Anchor, Index) local Max = max(Anchor, Index) ActiveInstance.Selected = {} ActiveInstance.Return = {} for I = Min, Max, 1 do insert(ActiveInstance.Selected, I) if I > #ActiveInstance.Directories then I = I - #ActiveInstance.Directories insert(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. ActiveInstance.Files[I]) else insert(ActiveInstance.Return, ActiveInstance.Directory .. "/" .. ActiveInstance.Directories[I]) end end end end end if Set then ActiveInstance.Selected = {Index} ActiveInstance.Return = {ActiveInstance.Directory .. "/" .. Label} end UpdateInputText(ActiveInstance) end local Result = false if ListBox.IsItemClicked(1, true) then if IsDirectory then OpenDirectory(ActiveInstance.Directory .. "/" .. Label) else Result = true end end ListBox.EndItem() return Result end local function AddDirectoryItem(Path) local Separator = FileSystem.Separator() local Item = {} Item.Path = Path Item.Name = FileSystem.GetBaseName(Path) Item.Name = Item.Name == "" and Separator or Item.Name -- Remove the starting slash for Unix style directories. if string.sub(Item.Name, 1, 1) == Separator and Item.Name ~= Separator then Item.Name = string.sub(Item.Name, 2) end Item.Children = nil return Item end local function FileDialogExplorer(Instance, Root) if Instance == nil then return end if Root ~= nil then local ShouldOpen = Window.IsAppearing() and string.find(Instance.Directory, Root.Path, 1, true) ~= nil local Options = { Label = Root.Name, OpenWithHighlight = false, IsSelected = ActiveInstance.Directory == Root.Path, IsOpen = ShouldOpen } local IsOpen = Tree.Begin(Root.Path, Options) if Mouse.IsClicked(1) and Window.IsItemHot() then OpenDirectory(Root.Path) end if IsOpen then if Root.Children == nil then Root.Children = {} local Separator = FileSystem.Separator() local Directories = FileSystem.GetDirectoryItems(Root.Path .. Separator, {Files = false}) for I, V in ipairs(Directories) do local Path = Root.Path if string.sub(Path, #Path) ~= Separator and Path ~= Separator then Path = Path .. Separator end if string.sub(V, 1, 1) == Separator then V = string.sub(V, 2) end local Item = AddDirectoryItem(Path .. FileSystem.GetBaseName(V)) insert(Root.Children, Item) end end for I, V in ipairs(Root.Children) do FileDialogExplorer(Instance, V) end Tree.End() end end end local function GetFilter(Instance, Index) local Filter = "*.*" local Desc = "All Files" if Instance ~= nil and #Instance.Filters > 0 then if Index == nil then Index = Instance.SelectedFilter end local Item = Instance.Filters[Index] if Item ~= nil then if type(Item) == "table" then if #Item == 1 then Filter = Item[1] Desc = "" elseif #Item == 2 then Filter = Item[1] Desc = Item[2] end else Filter = tostring(Item) Desc = "" end end end return Filter, Desc end local function GetExtension(Instance) local Filter, Desc = GetFilter(Instance) local Result = "" if Filter ~= "*.*" then local Index = string.find(Filter, ".", 1, true) if Index ~= nil then Result = string.sub(Filter, Index) end end return Result end local function IsInstanceOpen(Id) local Instance = Instances[Id] if Instance ~= nil then return Instance.IsOpen end return false end local function GetInstance(Id) if Instances[Id] == nil then local Instance = {} Instance.Id = Id Instance.IsOpen = false Instance.Opening = false Instance.W = 0.0 Instance.H = 0.0 Instances[Id] = Instance end return Instances[Id] end function Dialog.Begin(Id, Options) local Instance = GetInstance(Id) if not Instance.IsOpen then return false end Options = Options == nil and {} or Options Options.X = floor(Scale.GetScreenWidth() * 0.5 - Instance.W * 0.5) Options.Y = floor(Scale.GetScreenHeight() * 0.5 - Instance.H * 0.5) Options.Layer = 'Dialog' Options.AllowFocus = false Options.AllowMove = false Options.AutoSizeWindow = Options.AutoSizeWindow == nil and true or Options.AutoSizeWindow Options.NoSavedSettings = true Window.Begin(Instance.Id, Options) if Instance.Opening then Input.SetFocused(nil) Instance.Opening = false end ActiveInstance = Instance insert(InstanceStack, 1, ActiveInstance) return true end function Dialog.End() ActiveInstance.W, ActiveInstance.H = Window.GetSize() Window.End() ActiveInstance = nil remove(InstanceStack, 1) if #InstanceStack > 0 then ActiveInstance = InstanceStack[1] end end function Dialog.Open(Id) local Instance = GetInstance(Id) if not Instance.IsOpen then Instance.Opening = true Instance.IsOpen = true insert(Stack, 1, Instance) Window.SetStackLock(Instance.Id) Window.PushToTop(Instance.Id) end end function Dialog.Close() if ActiveInstance ~= nil and ActiveInstance.IsOpen then ActiveInstance.IsOpen = false remove(Stack, 1) Window.SetStackLock(nil) if #Stack > 0 then Instance = Stack[1] Window.SetStackLock(Instance.Id) Window.PushToTop(Instance.Id) end end end function Dialog.IsOpen() return #Stack > 0 end function Dialog.MessageBox(Title, Message, Options) local Result = "" Dialog.Open('MessageBox') if Dialog.Begin('MessageBox', {Title = Title, Border = 12}) then Options = Options == nil and {} or Options Options.Buttons = Options.Buttons == nil and {"OK"} or Options.Buttons LayoutManager.Begin('MessageBox_Message_Layout', {AlignX = 'center', AlignY = 'center'}) LayoutManager.NewLine() local TextW = min(Text.GetWidth(Message), Scale.GetScreenWidth() * 0.80) Text.BeginFormatted(Message, {Align = 'center', W = TextW}) LayoutManager.End() Cursor.NewLine() Cursor.NewLine() LayoutManager.Begin('MessageBox_Buttons_Layout', {AlignX = 'right', AlignY = 'bottom'}) for I, V in ipairs(Options.Buttons) do if Button.Begin(V) then Result = V end Cursor.SameLine() LayoutManager.SameLine() end LayoutManager.End() if Result ~= "" then Dialog.Close() end Dialog.End() end return Result end function Dialog.FileDialog(Options) Options = Options == nil and {} or Options Options.AllowMultiSelect = Options.AllowMultiSelect == nil and true or Options.AllowMultiSelect Options.Directory = Options.Directory == nil and nil or Options.Directory Options.Type = Options.Type == nil and 'openfile' or Options.Type Options.Title = Options.Title == nil and nil or Options.Title Options.Filters = Options.Filters == nil and {{"*.*", "All Files"}} or Options.Filters Options.IncludeParent = Options.IncludeParent == nil and true or Options.IncludeParent if Options.Title == nil then Options.Title = "Open File" if Options.Type == 'savefile' then Options.AllowMultiSelect = false Options.Title = "Save File" elseif Options.Type == 'opendirectory' then Options.Title = "Open Directory" end end local Result = {Button = "", Files = {}} local WasOpen = IsInstanceOpen('FileDialog') Dialog.Open("FileDialog") local W = Scale.GetScreenWidth() * 0.65 local H = Scale.GetScreenHeight() * 0.65 if Dialog.Begin('FileDialog', { Title = Options.Title, AutoSizeWindow = false, W = W, H = H, AutoSizeContent = false, AllowResize = false }) then ActiveInstance.AllowMultiSelect = Options.AllowMultiSelect if not WasOpen then ActiveInstance.Text = "" if ActiveInstance.Directory == nil then ActiveInstance.Directory = love.filesystem.getSourceBaseDirectory() end if Options.Directory ~= nil and FileSystem.IsDirectory(Options.Directory) then ActiveInstance.Directory = Options.Directory end ActiveInstance.Filters = Options.Filters ActiveInstance.SelectedFilter = 1 end local Clear = false if not ActiveInstance.Parsed then local Filter = GetFilter(ActiveInstance) ActiveInstance.Root = AddDirectoryItem(FileSystem.GetRootDirectory(ActiveInstance.Directory)) ActiveInstance.Selected = {} ActiveInstance.Directories = FileSystem.GetDirectoryItems(ActiveInstance.Directory .. "/", {Files = false}) ActiveInstance.Files = FileSystem.GetDirectoryItems(ActiveInstance.Directory .. "/", {Directories = false, Filter = Filter}) ActiveInstance.Return = {ActiveInstance.Directory .. "/"} ActiveInstance.Text = "" ActiveInstance.Parsed = true UpdateInputText(ActiveInstance) for I, V in ipairs(ActiveInstance.Directories) do ActiveInstance.Directories[I] = FileSystem.GetBaseName(V) end for I, V in ipairs(ActiveInstance.Files) do ActiveInstance.Files[I] = FileSystem.GetBaseName(V) end Clear = true end local WinW, WinH = Window.GetSize() local ButtonW, ButtonH = Button.GetSize("OK") local ExplorerW = 150.0 local ListH = WinH - Text.GetHeight() - ButtonH * 3.0 - Cursor.PadY() * 2.0 local PrevAnchorX = Cursor.GetAnchorX() -- Parent directory button for quick access local FontH = Style.Font:getHeight() local UpImage = {Path = SLAB_FILE_PATH .. "/Internal/Resources/Textures/Icons.png", SubX = 50.0, SubY = 0.0, SubW = 50.0, SubH = 50.0} if Button.Begin("", {Image = UpImage, Color = {0, 0, 0, 0}, PadX = 2, PadY = 2, W = FontH, H = FontH}) then local Destination = FileSystem.Sanitize(ActiveInstance.Directory .. FileSystem.Separator() .. "..") -- Only attempt to move to parent directory if not the root drive. if not FileSystem.IsDrive(Destination) then OpenDirectory(Destination) end end -- TODO: Place in region so that it can be scrolled. Cursor.SameLine() local CursorX, CursorY = Cursor.GetPosition() local RemainingW, RemianingH = Window.GetRemainingSize() local MouseX, MouseY = Window.GetMousePosition() Region.Begin('FileDialog_BreadCrumbs', { X = CursorX, Y = CursorY, W = RemainingW, H = FontH, AutoSizeContent = true, NoBackground = true, Intersect = true, MouseX = MouseX, MouseY = MouseY, IsObstructed = Window.IsObstructedAtMouse(), Rounding = Style.Rounding, IgnoreScroll = true }) -- Add some padding from left border. The cursor internally adds it's own padding. Cursor.AdvanceX(0.0) -- Gather each directory name and render as bread crumbs on top of each view. local Tokens = {} for Token in gmatch(ActiveInstance.Directory, "([^" .. FileSystem.Separator() .. "]+)") do insert(Tokens, Token) end for I, Token in ipairs(Tokens) do Window.PushID(Token .. '_Crumb') local Clicked = Text.Begin(Token, {IsSelectableTextOnly = true}) -- Render an arrow between elements to provide spacing. if I < #Tokens then Cursor.SameLine() Image.Begin(Token .. '_Crumb', {Path = UpImage.Path, SubX = 100.0, SubY = 0.0, SubW = 50.0, SubH = 50.0, W = FontH, H = FontH}) Cursor.SameLine() end if Clicked then local Destination = nil for J = 1, I, 1 do Destination = Destination and (Destination .. FileSystem.Separator() .. Tokens[J]) or Tokens[J] end if Destination ~= nil then OpenDirectory(Destination) end end Window.PopID() end -- Move the region's scrollable area to always have the current directory in view. local ContentW, ContentH = Region.GetContentSize() Region.ResetTransform() Region.Translate(nil, math.min(RemainingW - ContentW - 4.0, 0.0), 0.0) Region.End() Region.ApplyScissor() CursorX, CursorY = Cursor.GetPosition() MouseX, MouseY = Window.GetMousePosition() Region.Begin('FileDialog_DirectoryExplorer', { X = CursorX, Y = CursorY, W = ExplorerW, H = ListH, AutoSizeContent = true, NoBackground = true, Intersect = true, MouseX = MouseX, MouseY = MouseY, IsObstructed = Window.IsObstructedAtMouse(), Rounding = Style.WindowRounding }) Cursor.AdvanceX(0.0) Cursor.SetAnchorX(Cursor.GetX()) FileDialogExplorer(ActiveInstance, ActiveInstance.Root) Region.End() Region.ApplyScissor() Cursor.AdvanceX(ExplorerW + 4.0) Cursor.SetY(CursorY) LayoutManager.Begin('FileDialog_ListBox_Expand', {AnchorX = true, ExpandW = true}) ListBox.Begin('FileDialog_ListBox', {H = ListH, Clear = Clear}) local Index = 1 local ItemSelected = false if Options.IncludeParent then if FileDialogItem('Item_Parent', "..", true, Index) then ItemSelected = true end Index = Index + 1 end for I, V in ipairs(ActiveInstance.Directories) do FileDialogItem('Item_' .. Index, V, true, Index) Index = Index + 1 end if Options.Type ~= 'opendirectory' then for I, V in ipairs(ActiveInstance.Files) do if FileDialogItem('Item_' .. Index, V, false, Index) then ItemSelected = true end Index = Index + 1 end end ListBox.End() LayoutManager.End() local ListBoxX, ListBoxY, ListBoxW, ListBoxH = Cursor.GetItemBounds() local InputW = ListBoxX + ListBoxW - PrevAnchorX - FilterW - Cursor.PadX() Cursor.SetAnchorX(PrevAnchorX) Cursor.SetX(PrevAnchorX) local ReadOnly = Options.Type ~= 'savefile' if Input.Begin('FileDialog_Input', {W = InputW, ReadOnly = ReadOnly, Text = ActiveInstance.Text, Align = 'left'}) then ActiveInstance.Text = Input.GetText() ActiveInstance.Return[1] = ActiveInstance.Text end Cursor.SameLine() local Filter, Desc = GetFilter(ActiveInstance) if ComboBox.Begin('FileDialog_Filter', {Selected = Filter .. " " .. Desc}) then for I, V in ipairs(ActiveInstance.Filters) do Filter, Desc = GetFilter(ActiveInstance, I) if Text.Begin(Filter .. " " .. Desc, {IsSelectable = true}) then ActiveInstance.SelectedFilter = I ActiveInstance.Parsed = false end end ComboBox.End() end local FilterCBX, FilterCBY, FilterCBW, FilterCBH = Cursor.GetItemBounds() FilterW = FilterCBW LayoutManager.Begin('FileDialog_Buttons_Layout', {AlignX = 'right', AlignY = 'bottom'}) if Button.Begin("OK") or ItemSelected then local OpeningDirectory = false if #ActiveInstance.Return == 1 and Options.Type ~= 'opendirectory' then local Path = ActiveInstance.Return[1] if FileSystem.IsDirectory(Path) then OpeningDirectory = true OpenDirectory(Path) elseif Options.Type == 'savefile' then if FileSystem.Exists(Path) then FileDialog_AskOverwrite = true OpeningDirectory = true end end end if not OpeningDirectory then Result.Button = "OK" Result.Files = PruneResults(ActiveInstance.Return, Options.Type == 'opendirectory') if Options.Type == 'savefile' then ValidateSaveFile(Result.Files, GetExtension(ActiveInstance)) end end end Cursor.SameLine() LayoutManager.SameLine() if Button.Begin("Cancel") then Result.Button = "Cancel" end LayoutManager.End() if FileDialog_AskOverwrite then local FileName = #ActiveInstance.Return > 0 and ActiveInstance.Return[1] or "" local AskOverwrite = Dialog.MessageBox("Overwriting", "Are you sure you would like to overwrite file " .. FileName, {Buttons = {"Cancel", "No", "Yes"}}) if AskOverwrite ~= "" then if AskOverwrite == "No" then Result.Button = "Cancel" Result.Files = {} elseif AskOverwrite == "Yes" then Result.Button = "OK" Result.Files = PruneResults(ActiveInstance.Return, Options.Type == 'opendirectory') end FileDialog_AskOverwrite = false end end if Result.Button ~= "" then ActiveInstance.Parsed = false Dialog.Close() end Dialog.End() end return Result end return Dialog ================================================ FILE: Internal/UI/Dock.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local MenuState = require(SLAB_PATH .. '.Internal.UI.MenuState') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Style = require(SLAB_PATH .. '.Style') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local Dock = {} local Instances = {} local Pending = nil local PendingWindow = nil local function IsValid(Id) if Id == nil then return false end if type(Id) ~= 'string' then return false end return Id == 'Left' or Id == 'Bottom' or Id == 'Right' end local function GetInstance(Id) if Instances[Id] == nil then local Instance = {} Instance.Id = Id Instance.Window = nil Instance.Reset = false Instance.TearX = 0 Instance.TearY = 0 Instance.IsTearing = false Instance.Torn = false Instance.CachedOptions = nil Instance.Enabled = true Instance.NoSavedSettings = false Instances[Id] = Instance end return Instances[Id] end local function GetOverlayBounds(Type) local X, Y, W, H = 0, 0, 0, 0 local ViewW, ViewH = Scale.GetScreenDimensions() local Offset = 75 if Type == 'Left' then W = 100 H = 150 X = Offset Y = ViewH * 0.5 - H * 0.5 elseif Type == 'Right' then W = 100 H = 150 X = ViewW - Offset - W Y = ViewH * 0.5 - H * 0.5 elseif Type == 'Bottom' then W = ViewW * 0.55 H = 100 X = ViewW * 0.5 - W * 0.5 Y = ViewH - Offset - H end return X, Y, W, H end local function DrawOverlay(Type) local Instance = GetInstance(Type) if Instance ~= nil and Instance.Window ~= nil then return end if not Instance.Enabled then return end local X, Y, W, H = GetOverlayBounds(Type) local Color = {0.29, 0.59, 0.83, 0.65} local TitleH = 14 local Spacing = 6 local MouseX, MouseY = Mouse.Position() if X <= MouseX and MouseX <= X + W and Y <= MouseY and MouseY <= Y + H then Color = {0.50, 0.75, 0.96, 0.65} Pending = Type end DrawCommands.Rectangle('fill', X, Y, W, TitleH, Color) DrawCommands.Rectangle('line', X, Y, W, TitleH, {0, 0, 0, 1}) Y = Y + TitleH + Spacing H = H - TitleH - Spacing DrawCommands.Rectangle('fill', X, Y, W, H, Color) DrawCommands.Rectangle('line', X, Y, W, H, {0, 0, 0, 1}) end function Dock.DrawOverlay() Pending = nil DrawCommands.SetLayer('Dock') DrawCommands.Begin() DrawOverlay('Left') DrawOverlay('Right') DrawOverlay('Bottom') DrawCommands.End() if Mouse.IsReleased(1) then for Id, Instance in pairs(Instances) do Instance.IsTearing = false end end end function Dock.Override() if Pending ~= nil and PendingWindow ~= nil then local Instance = GetInstance(Pending) Instance.Window = PendingWindow.Id Instance.Reset = true PendingWindow = nil Pending = nil end end function Dock.Commit() if Pending ~= nil and PendingWindow ~= nil and Mouse.IsReleased(1) then local Instance = GetInstance(Pending) Instance.Window = PendingWindow.Id Instance.Reset = true PendingWindow = nil Pending = nil end end function Dock.GetDock(WinId) for K, V in pairs(Instances) do if V.Window == WinId then return K end end return nil end function Dock.GetBounds(Type, Options) local X, Y, W, H = 0, 0, 0, 0 local ViewW, ViewH = Scale.GetScreenDimensions() local MainMenuBarH = MenuState.MainMenuBarH local TitleH = Style.Font:getHeight() if Type == 'Left' then Y = MainMenuBarH W = Options.W or 150 H = ViewH - Y - TitleH elseif Type == 'Right' then X = ViewW - 150 Y = MainMenuBarH W = Options.W or 150 H = ViewH - Y - TitleH elseif Type == 'Bottom' then Y = ViewH - 150 W = ViewW H = Options.H or 150 end return X, Y, W, H end function Dock.AlterOptions(WinId, Options) Options = Options == nil and {} or Options for Id, Instance in pairs(Instances) do if Instance.Window == WinId then if Instance.Torn or not Instance.Enabled then Instance.Window = nil Utility.CopyValues(Options, Instance.CachedOptions) Instance.CachedOptions = nil Instance.Torn = false Options.ResetSize = true else if Instance.Reset then Instance.CachedOptions = { X = Options.X, Y = Options.Y, W = Options.W, H = Options.H, AllowMove = Options.AllowMove, Layer = Options.Layer, SizerFilter = Utility.Copy(Options.SizerFilter), AutoSizeWindow = Options.AutoSizeWindow, AutoSizeWindowW = Options.AutoSizeWindowW, AutoSizeWindowH = Options.AutoSizeWindowH, AllowResize = Options.AllowResize } end Options.AllowMove = false Options.Layer = 'Dock' if Id == 'Left' then Options.SizerFilter = {'E'} elseif Id == 'Right' then Options.SizerFilter = {'W'} elseif Id == 'Bottom' then Options.SizerFilter = {'N'} end local X, Y, W, H = Dock.GetBounds(Id, Options) Options.X = X Options.Y = Y Options.W = W Options.H = H Options.AutoSizeWindow = false Options.AutoSizeWindowW = false Options.AutoSizeWindowH = false Options.AllowResize = true Options.ResetPosition = Instance.Reset Options.ResetSize = Instance.Reset Instance.Reset = false end break end end end function Dock.SetPendingWindow(Instance, Type) PendingWindow = Instance Pending = Type or Pending end function Dock.GetPendingWindow() return PendingWindow end function Dock.IsTethered(WinId) for Id, Instance in pairs(Instances) do if Instance.Window == WinId then return not Instance.Torn end end return false end function Dock.BeginTear(WinId, X, Y) for Id, Instance in pairs(Instances) do if Instance.Window == WinId then Instance.TearX = X Instance.TearY = Y Instance.IsTearing = true end end end function Dock.UpdateTear(WinId, X, Y) for Id, Instance in pairs(Instances) do if Instance.Window == WinId and Instance.IsTearing then local Threshold = 25.0 local DistanceX = Instance.TearX - X local DistanceY = Instance.TearY - Y local DistanceSq = DistanceX * DistanceX + DistanceY * DistanceY if DistanceSq >= Threshold * Threshold then Instance.IsTearing = false Instance.Torn = true end end end end function Dock.GetCachedOptions(WinId) for Id, Instance in pairs(Instances) do if Instance.Window == WinId then return Instance.CachedOptions end end return nil end function Dock.Toggle(List, Enabled) List = List == nil and {} or List Enabled = Enabled == nil and true or Enabled if type(List) == 'string' then List = {List} end for I, V in ipairs(List) do if IsValid(V) then local Instance = GetInstance(V) Instance.Enabled = Enabled end end end function Dock.SetOptions(Type, Options) Options = Options == nil and {} or Options Options.NoSavedSettings = Options.NoSavedSettings == nil and false or Options.NoSavedSettings if IsValid(Type) then local Instance = GetInstance(Type) Instance.NoSavedSettings = Options.NoSavedSettings end end function Dock.Save(Table) if Table ~= nil then local taken = {} local Settings = {} for K, V in pairs(Instances) do if not V.NoSavedSettings and V.Window and not taken[V.Window] then if V.Window then taken[V.Window] = true end Settings[K] = tostring(V.Window) end end Table['Dock'] = Settings end end function Dock.Load(Table) if Table ~= nil then local Settings = Table['Dock'] if Settings ~= nil then for K, V in pairs(Settings) do local Instance = GetInstance(K) Instance.Window = V end end end end return Dock ================================================ FILE: Internal/UI/Image.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Tooltip = require(SLAB_PATH .. '.Internal.UI.Tooltip') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local IdCache = require(SLAB_PATH .. '.Internal.Core.IdCache') local Image = {} local instances = {} local imageCache = {} local idCache = IdCache() local EMPTY = {} local WHITE = { 1, 1, 1, 1 } local BLACK = { 0, 0, 0, 1 } local function GetImage(path) if imageCache[path] == nil then imageCache[path] = love.graphics.newImage(path) end return imageCache[path] end local function GetInstance(id) local key = idCache:get(Window.GetId(), id) local instance = instances[key] if instance then return instance end instance = {} instance.Id = id instance.Image = nil instances[key] = instance return instance end function Image.Begin(id, options) local statHandle = Stats.Begin('Image', 'Slab') options = options or EMPTY local rotation = options.Rotation or 0 local scale = options.Scale or 1 local scaleX = options.ScaleX or scale local scaleY = options.ScaleY or scale local color = options.Color or WHITE local subW = options.SubW or 0.0 local subH = options.SubH or 0.0 local instance = GetInstance(id) local winItemId = Window.GetItemId(id) if instance.Image == nil then if options.Image == nil then assert(options.Path ~= nil, "Path to an image is required if no image is set!") instance.Image = GetImage(options.Path) else instance.Image = options.Image end elseif options.Image then if instance.Image ~= options.Image then instance.Image = options.Image end end instance.Image:setWrap(options.WrapH or "clamp", options.WrapV or "clamp") local w = options.W or instance.Image:getWidth() local h = options.H or instance.Image:getHeight() -- The final width and height setting will be what the developer requested if it exists. The scale factor will be calculated here. scaleX = options.W and (options.W / instance.Image:getWidth()) or scaleX scaleY = options.H and (options.H / instance.Image:getHeight()) or scaleY local hasExplicitSize = options.W and options.H if not hasExplicitSize then -- if the size isn't explictly defined, then apply scaling to the size. -- (If size is explicit, don't apply scaling, because the w,h are already exactly correct.) w = w * scaleX h = h * scaleY end local useSubImage = subW > 0.0 and subH > 0.0 if useSubImage then scaleX = options.W and (options.W / subW) or scaleX scaleY = options.H and (options.H / subH) or scaleY end w, h = LayoutManager.ComputeSize(w, h) LayoutManager.AddControl(w, h, 'Image') local x, y = Cursor.GetPosition() do local mouseX, mouseY = Window.GetMousePosition() if not Window.IsObstructedAtMouse() and x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then Tooltip.Begin(options.Tooltip or "") Window.SetHotItem(winItemId) end end if useSubImage then DrawCommands.SubImage( x, y, instance.Image, options.SubX or 0, options.SubY or 0, subW, subH, rotation, scaleX, scaleY, color) else DrawCommands.Image(x, y, instance.Image, rotation, scaleX, scaleY, color) end if options.UseOutline then DrawCommands.Rectangle( 'line', x, y, useSubImage and subW or w, useSubImage and subH or h, options.OutlineColor or BLACK, nil, nil, options.OutlineW or 1 ) end Cursor.SetItemBounds(x, y, w, h) Cursor.AdvanceY(h) Window.AddItem(x, y, w, h, winItemId) Stats.End(statHandle) end function Image.GetSize(Image) if Image ~= nil then local Data = Image if type(Image) == 'string' then Data = GetImage(Image) end if Data ~= nil then return Data:getWidth(), Data:getHeight() end end return 0, 0 end return Image ================================================ FILE: Internal/UI/Input.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local abs = math.abs local insert = table.insert local min = math.min local max = math.max local floor = math.floor local huge = math.huge local gsub = string.gsub local sub = string.sub local match = string.match local len = string.len local byte = string.byte local find = string.find local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local FileSystem = require(SLAB_PATH .. '.Internal.Core.FileSystem') local Keyboard = require(SLAB_PATH .. '.Internal.Input.Keyboard') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Tooltip = require(SLAB_PATH .. '.Internal.UI.Tooltip') local UTF8 = require('utf8') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Input = {} local Instances = {} local Focused = nil local LastFocused = nil local TextCursorPos = 0 local TextCursorPosLine = 0 local TextCursorPosLineMax = 0 local TextCursorPosLineNumber = 1 local TextCursorAnchor = -1 local TextCursorAlpha = 0.0 local FadeIn = true local DragSelect = false local FocusToNext = false local LastText = "" local Pad = Region.GetScrollPad() + Region.GetScrollBarSize() local PendingFocus = nil local PendingCursorPos = -1 local PendingCursorColumn = -1 local PendingCursorLine = -1 local IsSliding = false local DragDelta = 0 local MIN_WIDTH = 150.0 local DEF_PW_CHAR = "*" local LINE_NUMBER_W = 32 local function SanitizeText(Data) local Result = false if Data ~= nil then local Count = 0 Data, Count = gsub(Data, "\r", "") Result = Count > 0 end return Data, Result end local function UpdateLineNumbers(Instance, W, H, n_right, color) if not Instance.LineNumbers then return end n_right = n_right or 0 local n_lines = max(H/Text.GetHeight(), n_right) local line_numbers = {} for i = 0, n_lines do table.insert(line_numbers, Instance.LineNumbersStart + i) end local lines = table.concat(line_numbers, "\r\n") if color then local colored_lines = {color, lines} Instance.LineNumbersObject:setf(colored_lines, W - 2, "right") else Instance.LineNumbersObject:setf(lines, W - 2, "right") end end local function GetDisplayCharacter(Data, Pos) local Result = '' if Data ~= nil and Pos > 0 and Pos < len(Data) then local Offset = UTF8.offset(Data, -1, Pos + 1) Result = sub(Data, Offset, Pos) if Result == nil then Result = 'nil' end end if Result == '\n' then Result = "\\n" end return Result end local function GetCharacter(Data, Index, Forward) local Result = "" if Forward then local Sub = sub(Data, Index + 1) Result = match(Sub, "[%z\1-\127\194-\244%s\n][\128-\191]*") else local Sub = sub(Data, 1, Index) Result = match(Sub, "[%z\1-\127\194-\244%s\n][\128-\191]*$") end return Result end local function UpdateMultiLinePosition(Instance) if Instance == nil then return end if Instance.Lines ~= nil then local Count = 0 local Start = 0 local Found = false for I, V in ipairs(Instance.Lines) do local Length = len(V) Count = Count + Length if TextCursorPos < Count then TextCursorPosLine = TextCursorPos - Start TextCursorPosLineNumber = I Found = true break end Start = Start + Length end if not Found then TextCursorPosLine = len(Instance.Lines[#Instance.Lines]) TextCursorPosLineNumber = #Instance.Lines end else TextCursorPosLine = TextCursorPos TextCursorPosLineNumber = 1 end TextCursorPosLineMax = TextCursorPosLine end local function ValidateTextCursorPos(Instance) if Instance ~= nil then local OldPos = TextCursorPos local Byte = byte(sub(Instance.Text, TextCursorPos, TextCursorPos)) -- This is a continuation byte. Check next byte to see if it is an ASCII character or -- the beginning of a UTF8 character. if Byte ~= nil and Byte > 127 then local NextByte = byte(sub(Instance.Text, TextCursorPos + 1, TextCursorPos + 1)) if NextByte ~= nil and NextByte > 127 and NextByte < 191 then while Byte > 127 and Byte < 191 do TextCursorPos = TextCursorPos - 1 Byte = byte(sub(Instance.Text, TextCursorPos, TextCursorPos)) end if TextCursorPos < OldPos or Byte >= 191 then TextCursorPos = TextCursorPos - 1 UpdateMultiLinePosition(Instance) end end end end end local function MoveToHome(Instance) if Instance ~= nil then if Instance.Lines ~= nil and TextCursorPosLineNumber > 1 then TextCursorPosLine = 0 local Count = 0 local Start = 0 for I, V in ipairs(Instance.Lines) do Count = Count + len(V) if I == TextCursorPosLineNumber then TextCursorPos = Start break end Start = Start + len(V) end else TextCursorPos = 0 end UpdateMultiLinePosition(Instance) end end local function MoveToEnd(Instance) if Instance ~= nil then if Instance.Lines ~= nil then local Count = 0 for I, V in ipairs(Instance.Lines) do Count = Count + len(V) if I == TextCursorPosLineNumber then TextCursorPos = Count - 1 if I == #Instance.Lines then TextCursorPos = Count end break end end else TextCursorPos = #Instance.Text end UpdateMultiLinePosition(Instance) end end local function ValidateNumber(Instance) local Result = false if Instance ~= nil and Instance.NumbersOnly and Instance.Text ~= "" then if sub(Instance.Text, #Instance.Text, #Instance.Text) == "." then return end local Value = tonumber(Instance.Text) if Value == nil then Value = 0.0 end local OldValue = Value if Instance.MinNumber ~= nil then Value = max(Value, Instance.MinNumber) end if Instance.MaxNumber ~= nil then Value = min(Value, Instance.MaxNumber) end Result = OldValue ~= Value Instance.Text = tostring(Value) end return Result end local function GetAlignmentOffset(Instance) local Offset = 6.0 if Instance ~= nil then if Instance.Align == 'center' then local TextW = Text.GetWidth(Instance.Text) Offset = (Instance.W * 0.5) - (TextW * 0.5) end end return Offset end local function GetSelection(Instance) if Instance ~= nil and TextCursorAnchor >= 0 and TextCursorAnchor ~= TextCursorPos then local Min = min(TextCursorAnchor, TextCursorPos) + 1 local Max = max(TextCursorAnchor, TextCursorPos) return sub(Instance.Text, Min, Max) end return "" end local function MoveCursorVertical(Instance, MoveDown) if (Instance == nil) or (Instance.Lines == nil) then return end local OldLineNumber = TextCursorPosLineNumber if MoveDown then TextCursorPosLineNumber = min(TextCursorPosLineNumber + 1, #Instance.Lines) else TextCursorPosLineNumber = max(1, TextCursorPosLineNumber - 1) end local Line = Instance.Lines[TextCursorPosLineNumber] if OldLineNumber == TextCursorPosLineNumber then TextCursorPosLine = MoveDown and len(Line) or 0 else if TextCursorPosLineNumber == #Instance.Lines and TextCursorPosLine >= len(Line) then TextCursorPosLine = len(Line) else TextCursorPosLine = min(len(Line), TextCursorPosLineMax + 1) local Ch = GetCharacter(Line, TextCursorPosLine) if Ch ~= nil then TextCursorPosLine = TextCursorPosLine - len(Ch) end end end local Start = 0 for I, V in ipairs(Instance.Lines) do if I == TextCursorPosLineNumber then TextCursorPos = Start + TextCursorPosLine break end Start = Start + len(V) end end local function IsValidDigit(Instance, Ch) if Instance ~= nil then if Instance.NumbersOnly then if match(Ch, "%d") ~= nil then return true end if Ch == "-" then if TextCursorAnchor == 0 or TextCursorPos == 0 or #Instance.Text == 0 then return true end end if Ch == "." then local Selected = GetSelection(Instance) if Selected ~= nil and find(Selected, ".", 1, true) ~= nil then return true end if find(Instance.Text, ".", 1, true) == nil then return true end end else return true end end return false end local function IsCommandKeyDown() local LKey, RKey = 'lctrl', 'rctrl' if Utility.IsOSX() then LKey, RKey = 'lgui', 'rgui' end return Keyboard.IsDown(LKey) or Keyboard.IsDown(RKey) end local function IsHomePressed() local Result = false if Utility.IsOSX() then Result = IsCommandKeyDown() and Keyboard.IsPressed('left') else Result = Keyboard.IsPressed('home') end return Result end local function IsEndPressed() local Result = false if Utility.IsOSX() then Result = IsCommandKeyDown() and Keyboard.IsPressed('right') else Result = Keyboard.IsPressed('end') end return Result end local function IsNextSpaceDown() local Result = false if Utility.IsOSX() then Result = Keyboard.IsDown('lalt') or Keyboard.IsDown('ralt') else Result = Keyboard.IsDown('lctrl') or Keyboard.IsDown('rctrl') end return Result end local function GetCursorXOffset(Instance) local Result = GetAlignmentOffset(Instance) if Instance ~= nil then if TextCursorPos > 0 then local Sub = sub(Instance.Text, 1, TextCursorPos) Result = Text.GetWidth(Sub) + GetAlignmentOffset(Instance) end end return Result end local function GetCursorPos(Instance) local X, Y = GetAlignmentOffset(Instance), 0.0 if Instance ~= nil then local Data = Instance.Text if Instance.Lines ~= nil then Data = Instance.Lines[TextCursorPosLineNumber] Y = Text.GetHeight() * (TextCursorPosLineNumber - 1) end local CursorPos = min(TextCursorPosLine, len(Data)) if CursorPos > 0 then local Sub = sub(Data, 0, CursorPos) X = X + Text.GetWidth(Sub) end end return X, Y end local function SelectWord(Instance) if Instance ~= nil then local Filter = "%s" if GetCharacter(Instance.Text, TextCursorPos) == " " then if GetCharacter(Instance.Text, TextCursorPos + 1) == " " then Filter = "%S" else TextCursorPos = TextCursorPos + 1 end end TextCursorAnchor = 0 local I = 0 while I ~= nil and I + 1 < TextCursorPos do I = find(Instance.Text, Filter, I + 1) if I ~= nil and I < TextCursorPos then TextCursorAnchor = I else break end end I = find(Instance.Text, Filter, TextCursorPos + 1) if I ~= nil then TextCursorPos = I - 1 else TextCursorPos = #Instance.Text end UpdateMultiLinePosition(Instance) end end local function GetNextCursorPos(Instance, Left) local Result = 0 if Instance ~= nil then local NextSpace = IsNextSpaceDown() if NextSpace then if Left then Result = 0 local I = 0 while I ~= nil and I + 1 < TextCursorPos do I = find(Instance.Text, "%s", I + 1) if I ~= nil and I < TextCursorPos then Result = I else break end end else local I = find(Instance.Text, "%s", TextCursorPos + 1) if I ~= nil then Result = I else Result = #Instance.Text end end else if Left then local Ch = GetCharacter(Instance.Text, TextCursorPos) if Ch ~= nil then Result = TextCursorPos - len(Ch) end else local Ch = GetCharacter(Instance.Text, TextCursorPos, true) if Ch ~= nil then Result = TextCursorPos + len(Ch) else Result = TextCursorPos end end end Result = max(0, Result) Result = min(Result, len(Instance.Text)) end return Result end local function GetCursorPosLine(Instance, Line, X) local Result = 0 if Instance ~= nil and Line ~= "" then if Text.GetWidth(Line) < X then Result = len(Line) if find(Line, "\n") ~= nil then Result = len(Line) - 1 end else X = X - GetAlignmentOffset(Instance) local PosX = X local Index = 0 local Sub = "" while Index <= len(Line) do local Ch = GetCharacter(Line, Index, true) if Ch == nil then break end Index = Index + len(Ch) Sub = Sub .. Ch local PosX = Text.GetWidth(Sub) if PosX > X then local CharX = PosX - X local CharW = Text.GetWidth(Ch) if CharX < CharW * 0.65 then Result = Result + len(Ch) end break end Result = Index end end end return Result end local function GetTextCursorPos(Instance, X, Y) local Result = 0 if Instance ~= nil then local Line = Instance.Text local Start = 0 if Instance.Lines ~= nil and #Instance.Lines > 0 then local H = Text.GetHeight() local LineNumber = 1 local Found = false for I, V in ipairs(Instance.Lines) do if Y <= H then Line = V Found = true break end H = H + Text.GetHeight() Start = Start + #V end if not Found then Line = Instance.Lines[#Instance.Lines] end end Result = min(Start + GetCursorPosLine(Instance, Line, X), #Instance.Text) end return Result end local function MoveCursorPage(Instance, PageDown) if Instance ~= nil then local PageH = Instance.H - Text.GetHeight() local PageY = PageDown and PageH or 0.0 local X, Y = GetCursorPos(Instance) local TX, TY = Region.InverseTransform(Instance.Id, 0.0, PageY) local NextY = 0.0 if PageDown then NextY = TY + PageH else NextY = max(TY - PageH, 0.0) end TextCursorPos = GetTextCursorPos(Instance, 0.0, NextY) UpdateMultiLinePosition(Instance) end end local function UpdateTransform(Instance) if Instance == nil then return end local X, Y = GetCursorPos(Instance) local TX, TY = Region.InverseTransform(Instance.Id, 0.0, 0.0) local W = TX + Instance.W - Region.GetScrollPad() - Region.GetScrollBarSize() local H = TY + Instance.H if Instance.H > Text.GetHeight() then H = H - Region.GetScrollPad() - Region.GetScrollBarSize() end local NewX = 0.0 if TextCursorPosLine == 0 then NewX = TX elseif X > W then NewX = -(X - W) elseif X < TX then NewX = TX - X end local NewY = 0.0 if TextCursorPosLineNumber == 1 then NewY = TY elseif Y > H then NewY = -(Y - H) elseif Y < TY then NewY = TY - Y end Region.Translate(Instance.Id, NewX, NewY) if Instance.LineNumbers then Region.Translate(Instance.Id .. "LineNumbers", NewX, NewY) end end local function DeleteSelection(Instance) if Instance ~= nil and Instance.Text ~= "" and not Instance.ReadOnly then local Start = 0 local Min = 0 local Max = 0 if TextCursorAnchor ~= -1 then Min = min(TextCursorAnchor, TextCursorPos) Max = max(TextCursorAnchor, TextCursorPos) + 1 else if TextCursorPos == 0 then return false end local NewTextCursorPos = TextCursorPos local Ch = GetCharacter(Instance.Text, TextCursorPos) if Ch ~= nil then Min = TextCursorPos - len(Ch) NewTextCursorPos = Min end Ch = GetCharacter(Instance.Text, TextCursorPos, true) if Ch ~= nil then Max = TextCursorPos + 1 else Max = len(Instance.Text) + 1 end TextCursorPos = NewTextCursorPos end local Left = sub(Instance.Text, 1, Min) local Right = sub(Instance.Text, Max) Instance.Text = Left .. Right if Instance.IsPassword then local Left = sub(Instance.OrigText, 1, Min) local Right = sub(Instance.OrigText, Max) Instance.OrigText = Left .. Right Instance.PasswordText = string.rep(Instance.PasswordChar, #Instance.OrigText) end TextCursorPos = len(Left) if TextCursorAnchor ~= -1 then TextCursorPos = min(TextCursorAnchor, TextCursorPos) end TextCursorPos = max(0, TextCursorPos) TextCursorPos = min(TextCursorPos, len(Instance.Text)) TextCursorAnchor = -1 UpdateMultiLinePosition(Instance) end return true end local function DrawSelection(Instance, X, Y, W, H, Color) if Instance ~= nil and TextCursorAnchor >= 0 and TextCursorAnchor ~= TextCursorPos then local Min = min(TextCursorAnchor, TextCursorPos) local Max = max(TextCursorAnchor, TextCursorPos) H = Text.GetHeight() if Instance.Lines ~= nil then local Count = 0 local Start = 0 local OffsetMin = 0 local OffsetMax = 0 local OffsetY = 0 for I, V in ipairs(Instance.Lines) do Count = Count + len(V) if Min < Count then if Min > Start then OffsetMin = max(Min - Start, 1) else OffsetMin = 0 end if Max < Count then OffsetMax = max(Max - Start, 1) else OffsetMax = len(V) end local SubMin = sub(V, 1, OffsetMin) local SubMax = sub(V, 1, OffsetMax) local MinX = Text.GetWidth(SubMin) - 1.0 + GetAlignmentOffset(Instance) local MaxX = Text.GetWidth(SubMax) + 1.0 + GetAlignmentOffset(Instance) DrawCommands.Rectangle('fill', X + MinX, Y + OffsetY, MaxX - MinX, H, Color) end if Max <= Count then break end Start = Start + len(V) OffsetY = OffsetY + H end else local SubMin = sub(Instance.Text, 1, Min) local SubMax = sub(Instance.Text, 1, Max) local MinX = Text.GetWidth(SubMin) - 1.0 + GetAlignmentOffset(Instance) local MaxX = Text.GetWidth(SubMax) + 1.0 + GetAlignmentOffset(Instance) DrawCommands.Rectangle('fill', X + MinX, Y, MaxX - MinX, H, Color) end end end local function DrawCursor(Instance, X, Y, W, H) if Instance ~= nil then local CX, CY = GetCursorPos(Instance) local CX = X + CX local CY = Y + CY H = Text.GetHeight() DrawCommands.Line(CX, CY, CX, CY + H, 1.0, {0.0, 0.0, 0.0, TextCursorAlpha}) end end local function IsHighlightTerminator(Ch) if Ch ~= nil then return match(Ch, "%w") == nil end return true end local function UpdateTextObject(Instance, Width, Align, Highlight, BaseColor) if (Instance == nil) or (Instance.TextObject == nil) then return end local ColoredText = {} if Highlight ~= nil then --local StartTime = love.timer.getTime() local _, TY = Region.InverseTransform(Instance.Id, 0, 0) local TextH = Text.GetHeight() local Top = TY - TextH * 2 local Bottom = TY + Instance.H + TextH * 2 local H = #Instance.Lines * TextH local TopLineNo = max(floor((Top / H) * #Instance.Lines), 1) local BottomLineNo = min(floor((Bottom / H) * #Instance.Lines), #Instance.Lines) local Index = 1 local EndIndex = 1 for I = 1, BottomLineNo, 1 do local Count = len(Instance.Lines[I]) if I < TopLineNo then Index = Index + Count end EndIndex = EndIndex + Count end if Index > 1 then insert(ColoredText, BaseColor) insert(ColoredText, sub(Instance.Text, 1, Index - 1)) end while Index < EndIndex do local MatchIndex = nil local Key = nil for K, V in pairs(Highlight) do local Found = nil local Anchor = Index repeat Found = find(Instance.Text, K, Anchor, true) if Found ~= nil then local FoundEnd = Found + len(K) local Prev = sub(Instance.Text, Found - 1, Found - 1) local Next = sub(Instance.Text, FoundEnd, FoundEnd) if Found == 1 then Prev = nil end if FoundEnd > len(Instance.Text) then Next = nil end if not (IsHighlightTerminator(Prev) and IsHighlightTerminator(Next)) then Anchor = Found + 1 Found = nil end else break end until Found ~= nil if Found ~= nil then if MatchIndex == nil then MatchIndex = Found Key = K elseif Found < MatchIndex then MatchIndex = Found Key = K end end end if Key ~= nil then insert(ColoredText, BaseColor) insert(ColoredText, sub(Instance.Text, Index, MatchIndex - 1)) insert(ColoredText, Highlight[Key]) insert(ColoredText, Key) Index = MatchIndex + len(Key) else insert(ColoredText, BaseColor) insert(ColoredText, sub(Instance.Text, Index, EndIndex)) Index = EndIndex break end end if Index < len(Instance.Text) then insert(ColoredText, BaseColor) insert(ColoredText, sub(Instance.Text, Index)) end --print(string.format("UpdateTextObject Time: %f", (love.timer.getTime() - StartTime))) end if #ColoredText == 0 then ColoredText = {BaseColor, Instance.Text} end Instance.TextObject:setf(ColoredText, Width, Align) end local function UpdateSlider(Instance, Precision) if Instance ~= nil then local Flag = true if Instance.NeedDrag then local DeltaX = Mouse.GetDelta() Flag = DeltaX ~= 0.0 end if Flag then local MouseX, _ = Mouse.Position() local MinX = Cursor.GetPosition() local MaxX = MinX + Instance.W local Ratio = Utility.Clamp((MouseX - MinX) / (MaxX - MinX), 0.0, 1.0) local Min = Instance.MinNumber == nil and -huge or Instance.MinNumber local Max = Instance.MaxNumber == nil and huge or Instance.MaxNumber local Value = (Max - Min) * Ratio + Min if Precision > 0 then Instance.Text = string.format("%." .. Precision .. "f", Value) else Instance.Text = string.format("%d", Value) end ValidateNumber(Instance); end end end local function UpdateDrag(Instance, Step) if Instance ~= nil then local DeltaX = Mouse.GetDelta() if DeltaX ~= 0.0 then -- The drag threshold will be calculated dynamically. This is achieved by taking the active monitor -- width and dividing by the allowable range. The DPI scale is taken into account as well. The -- threshold is clamped at 10 to prevent large requirements for drag effect. local DPIScale = love.window.getDPIScale() local Width, Height, Flags = love.window.getMode() local DesktopWidth, DesktopHeight = love.window.getDesktopDimensions(Flags.display) local Min = Instance.MinNumber or -huge local Max = Instance.MaxNumber or huge local Diff = (Max - Min) / Step local DragThreshold = 1.0 if Diff > 0 then DragThreshold = floor(DesktopWidth / Diff) / DPIScale DragThreshold = Utility.Clamp(DragThreshold, 1, 10) end DragDelta = DragDelta + DeltaX if abs(DragDelta) > DragThreshold then DragDelta = 0 local Value = tonumber(Instance.Text) if Value ~= nil then Value = Value + Step * (DeltaX < 0 and -1 or 1) Instance.Text = tostring(Value) ValidateNumber(Instance) end end end end end local function DrawSlider(Instance, DrawSliderAsHandle) if Instance ~= nil and Instance.NumbersOnly then local Value = tonumber(Instance.Text) if Value ~= nil then local Min = Instance.MinNumber == nil and -huge or Instance.MinNumber local Max = Instance.MaxNumber == nil and huge or Instance.MaxNumber local Ratio = (Value - Min) / (Max - Min) local SliderSize = 6.0 local MinX, MinY = Cursor.GetPosition() local MaxX, MaxY = MinX + Instance.W - SliderSize, MinY + Instance.H local X = (MaxX - MinX) * Ratio + MinX if DrawSliderAsHandle then DrawCommands.Rectangle('fill', X, MinY + 1.0, SliderSize, Instance.H - 2.0, Style.InputSliderColor) else local Padding = 2 DrawCommands.Rectangle('fill', MinX+Padding, MinY+Padding, Padding + (Instance.W - Padding * 3) * Ratio, Instance.H - (Padding * 2), Style.InputSliderColor) end end end end local function GetInstance(Id) for _, V in ipairs(Instances) do if V.Id == Id then return V end end local Instance = {} Instance.Id = Id Instance.Text = "" Instance.OrigText = "" Instance.PasswordText = "" Instance.TextChanged = false Instance.NumbersOnly = true Instance.ReadOnly = false Instance.Align = 'left' Instance.MinNumber = nil Instance.MaxNumber = nil Instance.Lines = nil Instance.TextObject = nil Instance.Highlight = nil Instance.ShouldUpdateTextObject = false Instance.LineNumbers = false Instance.LineNumbersStart = Instance.LineNumbers and 1 or nil Instance.LineNumbersObject = nil insert(Instances, Instance) return Instance end function Input.Begin(Id, Options) assert(Id ~= nil, "Please pass a valid Id into Slab.Input.") local StatHandle = Stats.Begin('Input', 'Slab') Options = Options or {} Options.Tooltip = Options.Tooltip or "" Options.ReturnOnText = Options.ReturnOnText == nil and true or Options.ReturnOnText Options.Text = Options.Text and tostring(Options.Text) or "" Options.TextColor = Options.TextColor Options.BgColor = Options.BgColor or Style.InputBgColor Options.SelectColor = Options.SelectColor or Style.InputSelectColor Options.SelectOnFocus = Options.SelectOnFocus == nil and true or Options.SelectOnFocus Options.W = Options.W Options.H = Options.H Options.ReadOnly = Options.ReadOnly or false Options.Align = Options.Align Options.Rounding = Options.Rounding or Style.InputBgRounding Options.MinNumber = Options.MinNumber Options.MaxNumber = Options.MaxNumber Options.MultiLine = Options.MultiLine or false Options.MultiLineW = Options.MultiLineW or huge Options.LineNumbers = Options.LineNumbers Options.LineNumbersStart = Options.LineNumbersStart or 1 Options.Highlight = Options.Highlight Options.Step = Options.Step or 1.0 Options.NoDrag = Options.NoDrag or false Options.UseSlider = Options.UseSlider or false Options.Precision = Options.Precision and math.floor(Utility.Clamp(Options.Precision, 0, 5)) or 3 Options.NeedDrag = Options.NeedDrag == nil and true or Options.NeedDrag Options.IsPassword = not not Options.IsPassword --default is false Options.PasswordChar = Options.IsPassword and Options.PasswordChar or DEF_PW_CHAR if type(Options.MinNumber) ~= "number" then Options.MinNumber = nil end if type(Options.MaxNumber) ~= "number" then Options.MaxNumber = nil end if Options.MultiLine then Options.TextColor = Style.MultilineTextColor end local Instance = GetInstance(Window.GetId() .. "." .. Id) Instance.NumbersOnly = Options.NumbersOnly Instance.ReadOnly = Options.ReadOnly Instance.Align = Options.Align Instance.MinNumber = Options.MinNumber Instance.MaxNumber = Options.MaxNumber Instance.MultiLine = Options.MultiLine Instance.LineNumbers = Options.LineNumbers Instance.LineNumbersStart = Options.LineNumbersStart Instance.NeedDrag = Options.NeedDrag Instance.IsPassword = Options.IsPassword Instance.PasswordChar = Options.PasswordChar if Instance.MultiLineW ~= Options.MultiLineW then Instance.Lines = nil end Instance.MultiLineW = Options.MultiLineW local WinItemId = Window.GetItemId(Id) if Instance.Align == nil then Instance.Align = (Instance == Focused and not IsSliding) and 'left' or 'center' if Instance.ReadOnly then Instance.Align = 'center' end if Options.MultiLine then Instance.Align = 'left' end end if Focused ~= Instance then if Options.MultiLine and #Options.Text ~= #Instance.Text then Instance.Lines = nil end Instance.Text = Options.Text == nil and Instance.Text or Options.Text if Options.IsPassword then Instance.Text = Instance.PasswordText == nil and Instance.Text or Instance.PasswordText end end if Instance.MinNumber ~= nil and Instance.MaxNumber ~= nil then assert(Instance.MinNumber <= Instance.MaxNumber, "Invalid MinNumber and MaxNumber passed to Input control '" .. Instance.Id .. "'. MinNumber: " .. Instance.MinNumber .. " MaxNumber: " .. Instance.MaxNumber) end local H = Options.H == nil and Text.GetHeight() or Options.H local W = Options.W == nil and MIN_WIDTH or Options.W local ContentW, ContentH = 0.0, 0.0 local Result = false W, H = LayoutManager.ComputeSize(W, H) LayoutManager.AddControl(W, H, 'Input') Instance.W = W Instance.H = H local X, Y = Cursor.GetPosition() if Options.MultiLine then Options.SelectOnFocus = false local WasSanitized = false Options.Text, WasSanitized = SanitizeText(Options.Text) if WasSanitized then Result = true LastText = Options.Text end ContentW, ContentH = Text.GetSizeWrap(Instance.Text, Options.MultiLineW) end local ShouldUpdateTextObject = Instance.ShouldUpdateTextObject Instance.ShouldUpdateTextObject = false if Options.MultiLine and (Instance.Lines == nil) and (Instance.Text ~= "") then if Instance.TextObject == nil then Instance.TextObject = love.graphics.newText(Style.Font) end Instance.Lines = Text.GetLines(Instance.Text, Options.MultiLineW) ContentH = #Instance.Lines * Text.GetHeight() ShouldUpdateTextObject = true end if Instance.MultiLine and Instance.LineNumbers then if Instance.LineNumbersObject == nil then Instance.LineNumbersObject = love.graphics.newText(Style.Font) UpdateLineNumbers(Instance, LINE_NUMBER_W, H, nil, Options.TextColor) end end if Options.Highlight ~= nil then if Instance.Highlight == nil or Utility.TableCount(Options.Highlight) ~= Utility.TableCount(Instance.Highlight) then Instance.Highlight = Utility.Copy(Options.Highlight) ShouldUpdateTextObject = true else for K, V in pairs(Options.Highlight) do local HighlightColor = Instance.Highlight[K] if HighlightColor ~= nil then if V[1] ~= HighlightColor[1] or V[2] ~= HighlightColor[2] or V[3] ~= HighlightColor[3] or V[4] ~= HighlightColor[4] then ShouldUpdateTextObject = true break end else Instance.Highlight = Utility.Copy(Options.Highlight) ShouldUpdateTextObject = true break end end end else if Instance.Highlight ~= nil then Instance.Highlight = nil ShouldUpdateTextObject = true end end if ShouldUpdateTextObject then UpdateLineNumbers(Instance, LINE_NUMBER_W, H, nil, Options.TextColor) UpdateTextObject( Instance, Options.MultiLineW, Instance.Align, Options.Highlight, Options.TextColor ) end local IsObstructed = Window.IsObstructedAtMouse() local MouseX, MouseY = Window.GetMousePosition() local Hovered = not IsObstructed and X <= MouseX and MouseX <= X + W and Y <= MouseY and MouseY <= Y + H local HoveredScrollBar = Region.IsHoverScrollBar(Instance.Id) or Region.IsScrolling() if Hovered and not HoveredScrollBar then Mouse.SetCursor('ibeam') Tooltip.Begin(Options.Tooltip) Window.SetHotItem(WinItemId) end local CheckFocus = Mouse.IsClicked(1) and not HoveredScrollBar local NumbersOnlyEntry = Mouse.IsDoubleClicked(1) and Instance.NumbersOnly local FocusedThisFrame = false local ClearFocus = false if CheckFocus then if Hovered then FocusedThisFrame = Focused ~= Instance Focused = Instance elseif Instance == Focused then ClearFocus = true Focused = nil end end if FocusToNext and LastFocused == nil then FocusedThisFrame = true Focused = Instance CheckFocus = true FocusToNext = false TextCursorAnchor = -1 TextCursorPos = 0 TextCursorPosLine = 0 TextCursorPosLineNumber = 1 end if LastFocused == Instance then LastFocused = nil end local IsEditing = Instance == Focused and not IsSliding if Instance == Focused then local Back = false -- local IgnoreBack = false local ShouldDelete = false local ShouldUpdateTransform = false -- local PreviousTextCursorPos = TextCursorPos if IsCommandKeyDown() then if Keyboard.IsPressed('x') or Keyboard.IsPressed('c') then local Selected = GetSelection(Instance) if Selected ~= "" then love.system.setClipboardText(Selected) ShouldDelete = Keyboard.IsPressed('x') end end if Keyboard.IsPressed('v') then local Text = FileSystem.GetClipboard() Input.Text(Text) TextCursorPos = min(TextCursorPos + #Text - 1, #Instance.Text) end end if Keyboard.IsPressed('tab') then if Options.MultiLine then Input.Text('\t') else LastFocused = Instance FocusToNext = true end end if Keyboard.IsPressed('backspace') then ShouldDelete = true -- IgnoreBack = TextCursorAnchor ~= -1 end if Keyboard.IsPressed('delete') then if TextCursorAnchor == -1 then local Ch = GetCharacter(Instance.Text, TextCursorPos, true) if Ch ~= nil then TextCursorPos = TextCursorPos + len(Ch) ShouldDelete = true end else -- IgnoreBack = true ShouldDelete = true end end if ShouldDelete then if DeleteSelection(Instance) then Instance.TextChanged = true end end local ClearAnchor = false local IsShiftDown = Keyboard.IsDown('lshift') or Keyboard.IsDown('rshift') if Keyboard.IsPressed('lshift') or Keyboard.IsPressed('rshift') then if TextCursorAnchor == -1 then TextCursorAnchor = TextCursorPos end end local HomePressed, EndPressed = false, false if IsHomePressed() then MoveToHome(Instance) ShouldUpdateTransform = true HomePressed = true end if IsEndPressed() then MoveToEnd(Instance) ShouldUpdateTransform = true EndPressed = true end if not HomePressed and (Keyboard.IsPressed('left') or Back) then TextCursorPos = GetNextCursorPos(Instance, true) ShouldUpdateTransform = true UpdateMultiLinePosition(Instance) end if not EndPressed and Keyboard.IsPressed('right') then TextCursorPos = GetNextCursorPos(Instance, false) ShouldUpdateTransform = true UpdateMultiLinePosition(Instance) end if Keyboard.IsPressed('up') then MoveCursorVertical(Instance, false) ShouldUpdateTransform = true end if Keyboard.IsPressed('down') then MoveCursorVertical(Instance, true) ShouldUpdateTransform = true end if Keyboard.IsPressed('pageup') then MoveCursorPage(Instance, false) ShouldUpdateTransform = true end if Keyboard.IsPressed('pagedown') then MoveCursorPage(Instance, true) ShouldUpdateTransform = true end if CheckFocus or DragSelect then if FocusedThisFrame then if Options.NumbersOnly and not NumbersOnlyEntry and not Options.NoDrag then IsSliding = true DragDelta = 0 elseif Options.SelectOnFocus and Instance.Text ~= "" then TextCursorAnchor = 0 TextCursorPos = #Instance.Text end -- Display the soft keyboard on mobile devices when an input control receives focus. if Utility.IsMobile() and not Options.ReadOnly then -- Always display for non numeric controls. If this control is a numeric input, check to make -- sure the user requested to add text for this numeric control. if not Options.NumbersOnly or NumbersOnlyEntry or Options.NoDrag then love.keyboard.setTextInput(true) end end -- Enable key repeat when an input control is focused. love.keyboard.setKeyRepeat(true) else local MouseInputX, MouseInputY = MouseX - X, MouseY - Y local CX, CY = Region.InverseTransform(Instance.Id, MouseInputX, MouseInputY) TextCursorPos = GetTextCursorPos(Instance, CX, CY) if Mouse.IsClicked(1) then TextCursorAnchor = TextCursorPos DragSelect = true end ShouldUpdateTransform = true IsShiftDown = true end UpdateMultiLinePosition(Instance) end if IsSliding then local Current = tonumber(Instance.Text) if Options.UseSlider then UpdateSlider(Instance, Options.Precision) else UpdateDrag(Instance, Options.Step) end Instance.TextChanged = Current ~= tonumber(Instance.Text) end if Mouse.IsReleased(1) then DragSelect = false if TextCursorAnchor == TextCursorPos then TextCursorAnchor = -1 end if IsSliding then IsSliding = false Focused = nil Result = true LastText = Instance.Text end end if Mouse.IsDoubleClicked(1) then local MouseInputX, MouseInputY = MouseX - X, MouseY - Y local CX, CY = Region.InverseTransform(Instance.Id, MouseInputX, MouseInputY) TextCursorPos = GetTextCursorPos(Instance, CX, CY) SelectWord(Instance) DragSelect = false end if Keyboard.IsPressed("return") or Keyboard.IsPressed("kpenter") then Result = true if Options.MultiLine then Input.Text('\n') else ClearFocus = true end end if Instance.TextChanged or Back then if Options.ReturnOnText then Result = true end if Options.MultiLine then Instance.Lines = Text.GetLines(Instance.Text, Options.MultiLineW) UpdateLineNumbers( Instance, LINE_NUMBER_W, H, #Instance.Lines, Options.TextColor ) UpdateTextObject( Instance, Options.MultiLineW, Instance.Align, Options.Highlight, Options.TextColor ) end UpdateMultiLinePosition(Instance) Instance.TextChanged = false -- PreviousTextCursorPos = -1 end if ShouldUpdateTransform then ClearAnchor = not IsShiftDown UpdateTransform(Instance) end if ClearAnchor then TextCursorAnchor = -1 end else local WasValidated = ValidateNumber(Instance) if WasValidated then Result = true LastText = Instance.Text end end if Region.IsScrolling(Instance.Id) then local DeltaX, DeltaY = Mouse.GetDelta() local WheelX, WheelY = Region.GetWheelDelta() if DeltaY ~= 0.0 or WheelY ~= 0.0 then Instance.ShouldUpdateTextObject = true end end if (Instance == Focused and not Instance.ReadOnly) or Options.MultiLine then Options.BgColor = Style.InputEditBgColor end local TX, TY = Window.TransformPoint(X, Y) if Instance.LineNumbers and (Instance.LineNumbersObject ~= nil) then Region.Begin(Instance.Id .. "LineNumbers", { X = X, Y = Y, W = LINE_NUMBER_W, H = H, ContentW = ContentW + Pad, ContentH = ContentH + Pad, BgColor = Options.BgColor, SX = TX, SY = TY, MouseX = MouseX, MouseY = MouseY, Intersect = true, IgnoreScroll = true, Rounding = Options.Rounding, IsObstructed = IsObstructed, AutoSizeContent = false }) LayoutManager.Begin("IgnoreLineNumbers", {Ignore = true}) Text.BeginObject(Instance.LineNumbersObject) LayoutManager.End() Region.End() Region.ApplyScissor() X = X + LINE_NUMBER_W end TX, TY = Window.TransformPoint(X, Y) Region.Begin(Instance.Id, { X = X, Y = Y, W = W, H = H, ContentW = ContentW + Pad, ContentH = ContentH + Pad, BgColor = Options.BgColor, SX = TX, SY = TY, MouseX = MouseX, MouseY = MouseY, Intersect = true, IgnoreScroll = not Options.MultiLine, Rounding = Options.Rounding, IsObstructed = IsObstructed, AutoSizeContent = false }) if Instance == Focused then if not IsSliding then DrawSelection(Instance, X, Y, W, H, Options.SelectColor) DrawCursor(Instance, X, Y, W, H) end end if Options.UseSlider then if not IsEditing then DrawSlider(Instance, Options.DrawSliderAsHandle) end end if Instance.Text ~= "" then Cursor.SetPosition(X + GetAlignmentOffset(Instance), Y) LayoutManager.Begin("Ignore", {Ignore = true}) if Instance.TextObject ~= nil then Text.BeginObject(Instance.TextObject) else Text.Begin(Instance.Text, {AddItem = false, Color = Options.TextColor}) end LayoutManager.End() end Region.End() Region.ApplyScissor() Cursor.SetItemBounds(X, Y, W, H) Cursor.SetPosition(X, Y) Cursor.AdvanceX(W) Cursor.AdvanceY(H) Window.AddItem(X, Y, W, H, WinItemId) if ClearFocus then ValidateNumber(Instance) LastText = Instance.Text Focused = nil if not Options.MultiLine then Region.ResetTransform(Instance.Id) end -- Close the soft keyboard on mobile platforms when an input control loses focus. if Utility.IsMobile() then love.keyboard.setTextInput(false) end -- Restore the key repeat flag to the state before an input control gained focus. love.keyboard.setKeyRepeat(false) end Stats.End(StatHandle) return Result end function Input.Text(Ch) if Focused ~= nil and not Focused.ReadOnly then if not IsValidDigit(Focused, Ch) then return end if TextCursorAnchor ~= -1 then DeleteSelection(Focused) end if TextCursorPos == 0 then Focused.Text = Ch .. Focused.Text Focused.OrigText = Ch .. Focused.OrigText else local Left = sub(Focused.Text, 0, TextCursorPos) local Right = sub(Focused.Text, TextCursorPos + 1) Focused.Text = Left .. Ch .. Right if Focused.IsPassword then local Left = sub(Focused.OrigText, 0, TextCursorPos) local Right = sub(Focused.OrigText, TextCursorPos + 1) Focused.OrigText = Left .. Ch .. Right end end if Focused.IsPassword then Focused.PasswordText = string.rep(Focused.PasswordChar, #Focused.OrigText) Focused.Text = Focused.PasswordText end TextCursorPos = min(TextCursorPos + len(Ch), len(Focused.Text)) TextCursorAnchor = -1 UpdateTransform(Focused) Focused.TextChanged = true end end function Input.Update(dt) local Delta = dt * 2.0 if FadeIn then TextCursorAlpha = min(TextCursorAlpha + Delta, 1.0) FadeIn = TextCursorAlpha < 1.0 else TextCursorAlpha = max(TextCursorAlpha - Delta, 0.0) FadeIn = TextCursorAlpha == 0.0 end if PendingFocus ~= nil then LastFocused = Focused Focused = PendingFocus PendingFocus = nil end if Focused ~= nil then if PendingCursorPos >= 0 then TextCursorPos = min(PendingCursorPos, #Focused.Text) ValidateTextCursorPos(Focused) UpdateMultiLinePosition(Focused) PendingCursorPos = -1 end local MultiLineChanged = false if PendingCursorColumn >= 0 then if Focused.Lines ~= nil then TextCursorPosLine = PendingCursorColumn MultiLineChanged = true end PendingCursorColumn = -1 end if PendingCursorLine > 0 then if Focused.Lines ~= nil then TextCursorPosLineNumber = min(PendingCursorLine, #Focused.Lines) MultiLineChanged = true end PendingCursorLine = 0 end if MultiLineChanged then local Line = Focused.Lines[TextCursorPosLineNumber] TextCursorPosLine = min(TextCursorPosLine, len(Line)) local Start = 0 for I, V in ipairs(Focused.Lines) do if I == TextCursorPosLineNumber then TextCursorPos = Start + TextCursorPosLine break end Start = Start + len(V) end ValidateTextCursorPos(Focused) end else PendingCursorPos = -1 PendingCursorColumn = -1 PendingCursorLine = 0 end end function Input.GetText() if Focused ~= nil then if Focused.NumbersOnly and (Focused.Text == "" or Focused.Text == ".") then return "0" end return Focused.IsPassword and Focused.OrigText or Focused.Text end return LastText end function Input.GetCursorPos() if Focused ~= nil then return TextCursorPos, TextCursorPosLine, TextCursorPosLineNumber end return 0, 0, 0 end function Input.IsAnyFocused() return Focused ~= nil end function Input.IsFocused(Id) local Instance = GetInstance(Window.GetId() .. '.' .. Id) return Instance == Focused end function Input.SetFocused(Id) if Id == nil then Focused = nil PendingFocus = nil return end local Instance = GetInstance(Window.GetId() .. '.' .. Id) PendingFocus = Instance end function Input.SetCursorPos(Pos) PendingCursorPos = max(Pos, 0) end function Input.SetCursorPosLine(Column, Line) if Column ~= nil then PendingCursorColumn = max(Column, 0) end if Line ~= nil then PendingCursorLine = max(Line, 1) end end function Input.GetDebugInfo() local Info = {} local X, Y = GetCursorPos(Focused) if Focused ~= nil then Region.InverseTransform(Focused.Id, X, Y) end Info['Focused'] = Focused ~= nil and Focused.Id or 'nil' Info['Width'] = Focused ~= nil and Focused.W or 0 Info['Height'] = Focused ~= nil and Focused.H or 0 Info['CursorX'] = X Info['CursorY'] = Y Info['CursorPos'] = TextCursorPos Info['Character'] = Focused ~= nil and GetDisplayCharacter(Focused.Text, TextCursorPos) or '' Info['LineCursorPos'] = TextCursorPosLine Info['LineCursorPosMax'] = TextCursorPosLineMax Info['LineNumber'] = TextCursorPosLineNumber Info['LineLength'] = (Focused ~= nil and Focused.Lines ~= nil) and len(Focused.Lines[TextCursorPosLineNumber]) or 0 Info['Lines'] = Focused ~= nil and Focused.Lines or nil return Info end return Input ================================================ FILE: Internal/UI/LayoutManager.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local remove = table.remove local max = math.max local min = math.min local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local LayoutManager = {} local Instances = {} local Stack = {} local Active = nil local function GetWindowBounds() local WinX, WinY, WinW, WinH = Window.GetBounds(true) local Border = Window.GetBorder() WinX = WinX + Border WinY = WinY + Border WinW = WinW - Border * 2 WinH = WinH - Border * 2 return WinX, WinY, WinW, WinH end local function GetRowSize(Instance) if Instance ~= nil then local Column = Instance.Columns[Instance.ColumnNo] if Column.Rows ~= nil then local Row = Column.Rows[Column.RowNo] if Row ~= nil then return Row.W, Row.H end end end return 0, 0 end local function GetRowCursorPos(Instance) if Instance ~= nil then local Column = Instance.Columns[Instance.ColumnNo] if Column.Rows ~= nil then local Row = Column.Rows[Column.RowNo] if Row ~= nil then return Row.CursorX, Row.CursorY end end end return nil, nil end local function GetLayoutH(Instance, IncludePad) IncludePad = IncludePad == nil and true or IncludePad if Instance ~= nil then local Column = Instance.Columns[Instance.ColumnNo] if Column.Rows ~= nil then local H = 0 for I, V in ipairs(Column.Rows) do H = H + V.H if IncludePad then H = H + Cursor.PadY() end end return H end end return 0 end local function GetPreviousRowBottom(Instance) if Instance ~= nil then local Column = Instance.Columns[Instance.ColumnNo] if Column.Rows ~= nil and Column.RowNo > 1 and Column.RowNo <= #Column.Rows then local Y = Column.Rows[Column.RowNo - 1].CursorY local H = Column.Rows[Column.RowNo - 1].H return Y + H end end return nil end local function GetColumnPosition(Instance) if Instance ~= nil then local WinX, WinY, WinW, WinH = GetWindowBounds() local WinL, WinT = Window.GetPosition() local Count = #Instance.Columns local ColumnW = WinW / Count local TotalW = 0 for I = 1, Instance.ColumnNo - 1, 1 do local Column = Instance.Columns[I] TotalW = TotalW + Column.W + Cursor.PadX() * 2 end local AnchorX, AnchorY = Instance.X, Instance.Y if not Instance.AnchorX then AnchorX = WinX - WinL - Window.GetBorder() end if not Instance.AnchorY then AnchorY = WinY - WinT - Window.GetBorder() end return AnchorX + TotalW, AnchorY end return 0, 0 end local function GetColumnSize(Instance) if Instance ~= nil then local Column = Instance.Columns[Instance.ColumnNo] local WinX, WinY, WinW, WinH = GetWindowBounds() local Count = #Instance.Columns local ColumnW = WinW / Count local W, H = ColumnW, GetLayoutH(Instance) if not Window.IsAutoSize() then H = WinH Column.W = W end return W, H end return 0, 0 end local function AddControl(Instance, W, H, Type) if Instance ~= nil then local RowW, RowH = GetRowSize(Instance) local WinX, WinY, WinW, WinH = GetWindowBounds() local CursorX, CursorY = Cursor.GetPosition() local X, Y = GetRowCursorPos(Instance) local LayoutH = GetLayoutH(Instance) local PrevRowBottom = GetPreviousRowBottom(Instance) local AnchorX, AnchorY = GetColumnPosition(Instance) WinW, WinH = GetColumnSize(Instance) local Column = Instance.Columns[Instance.ColumnNo] local Border = Window.GetBorder() if RowW == 0 then RowW = W end if RowH == 0 then RowH = H end if X == nil then if Instance.AlignX == 'center' then X = max(WinW * 0.5 - RowW * 0.5 + AnchorX, AnchorX) elseif Instance.AlignX == 'right' then local Right = WinW - RowW if not Window.IsAutoSize() then Right = Right + Window.GetBorder() end X = max(Right, AnchorX) - Border * 2 else X = AnchorX end end if Y == nil then if PrevRowBottom ~= nil then Y = PrevRowBottom + Cursor.PadY() else local RegionH = WinY + WinH - CursorY if Instance.AlignY == 'center' then Y = max(RegionH * 0.5 - LayoutH * 0.5 + AnchorY, AnchorY) elseif Instance.AlignY == 'bottom' then Y = max(WinH - LayoutH, AnchorY) else Y = AnchorY end end end Cursor.SetX(WinX + X) Cursor.SetY(WinY + Y) if H < RowH then if Instance.AlignRowY == 'center' then Cursor.SetY(WinY + Y + RowH * 0.5 - H * 0.5) elseif Instance.AlignRowY == 'bottom' then Cursor.SetY(WinY + Y + RowH - H) end end local RowNo = Column.RowNo if Column.Rows ~= nil then local Row = Column.Rows[RowNo] if Row ~= nil then Row.CursorX = X + W + Cursor.PadX() Row.CursorY = Y end end if Column.PendingRows[RowNo] == nil then local Row = { CursorX = nil, CursorY = nil, W = 0, H = 0, RequestH = 0, MaxH = 0, Controls = {} } insert(Column.PendingRows, Row) end local Row = Column.PendingRows[RowNo] insert(Row.Controls, { X = Cursor.GetX(), Y = Cursor.GetY(), W = W, H = H, AlteredSize = Column.AlteredSize, Type = Type }) Row.W = Row.W + W Row.H = max(Row.H, H) Column.RowNo = RowNo + 1 Column.AlteredSize = false Column.W = max(Row.W, Column.W) end end local function GetInstance(Id) local Key = Window.GetId() .. '.' .. Id if Instances[Key] == nil then local Instance = {} Instance.Id = Id Instance.WindowId = Window.GetId() Instance.AlignX = 'left' Instance.AlignY = 'top' Instance.AlignRowY = 'top' Instance.Ignore = false Instance.ExpandW = false Instance.ExpandH = false Instance.X = 0 Instance.Y = 0 Instance.Columns = {} Instance.ColumnNo = 1 Instances[Key] = Instance end return Instances[Key] end function LayoutManager.AddControl(W, H, Type) if Active ~= nil and (not Active.Ignore) then AddControl(Active, W, H, Type) end end function LayoutManager.ComputeSize(W, H) if Active ~= nil then local X, Y = GetColumnPosition(Active) local WinW, WinH = GetColumnSize(Active) local RealW = WinW - X local RealH = WinH - Y local Column = Active.Columns[Active.ColumnNo] if not Active.AnchorX then RealW = WinW end if not Active.AnchorY then RealH = WinH end -- Retrieve the calculated row width. This information is stored in the 'PendingRows' -- field of the active column. This information is updated in the 'AddControl' function. local Row = nil local RemainingW = WinW if Column.PendingRows ~= nil then Row = Column.PendingRows[Column.RowNo] if Row ~= nil then RemainingW = WinW - Row.W end end W = min(W, RemainingW) if Window.IsAutoSize() then local LayoutH = GetLayoutH(Active, false) if LayoutH > 0 then RealH = LayoutH end end if Active.ExpandW then if Column.Rows ~= nil then local Count = 0 local ReduceW = 0 local Pad = 0 local Row = Column.Rows[Column.RowNo] if Row ~= nil then for I, V in ipairs(Row.Controls) do if V.AlteredSize then Count = Count + 1 else ReduceW = ReduceW + V.W end end if #Row.Controls > 1 then Pad = Cursor.PadX() * (#Row.Controls - 1) end end Count = max(Count, 1) W = (RealW - ReduceW - Pad) / Count end end if Active.ExpandH then if Column.Rows ~= nil then local Count = 0 local ReduceH = 0 local Pad = 0 local MaxRowH = 0 for I, Row in ipairs(Column.Rows) do local IsSizeAltered = false if I == Column.RowNo then MaxRowH = Row.MaxH Row.RequestH = max(Row.RequestH, H) end for J, Control in ipairs(Row.Controls) do if Control.AlteredSize then if not IsSizeAltered then Count = Count + 1 IsSizeAltered = true end end end if not IsSizeAltered then ReduceH = ReduceH + Row.H end end if #Column.Rows > 1 then Pad = Cursor.PadY() * (#Column.Rows - 1) end Count = max(Count, 1) RealH = max(RealH - ReduceH - Pad, 0) H = max(RealH / Count, H) H = max(H, MaxRowH) end end Column.AlteredSize = Active.ExpandW or Active.ExpandH end return W, H end function LayoutManager.Begin(Id, Options) assert(Id ~= nil or type(Id) ~= string, "A valid string Id must be given to BeginLayout!") Options = Options == nil and {} or Options Options.AlignX = Options.AlignX == nil and 'left' or Options.AlignX Options.AlignY = Options.AlignY == nil and 'top' or Options.AlignY Options.AlignRowY = Options.AlignRowY == nil and 'top' or Options.AlignRowY Options.Ignore = Options.Ignore == nil and false or Options.Ignore Options.ExpandW = Options.ExpandW == nil and false or Options.ExpandW Options.ExpandH = Options.ExpandH == nil and false or Options.ExpandH Options.AnchorX = Options.AnchorX == nil and false or Options.AnchorX Options.AnchorY = Options.AnchorY == nil and true or Options.AnchorY Options.Columns = Options.Columns == nil and 1 or Options.Columns Options.Columns = max(Options.Columns, 1) local Instance = GetInstance(Id) Instance.AlignX = Options.AlignX Instance.AlignY = Options.AlignY Instance.AlignRowY = Options.AlignRowY Instance.Ignore = Options.Ignore Instance.ExpandW = Options.ExpandW Instance.ExpandH = Options.ExpandH Instance.X, Instance.Y = Cursor.GetRelativePosition() Instance.AnchorX = Options.AnchorX Instance.AnchorY = Options.AnchorY if Options.Columns ~= #Instance.Columns then Instance.Columns = {} for I = 1, Options.Columns, 1 do local Column = { Rows = nil, PendingRows = {}, RowNo = 1, W = 0 } insert(Instance.Columns, Column) end end for I, Column in ipairs(Instance.Columns) do Column.PendingRows = {} Column.RowNo = 1 end insert(Stack, 1, Instance) Active = Instance end function LayoutManager.End() assert(Active ~= nil, "LayoutManager.End was called without a call to LayoutManager.Begin!") for I, Column in ipairs(Active.Columns) do local Rows = Column.Rows Column.Rows = Column.PendingRows Column.PendingRows = nil if Rows ~= nil and Column.Rows ~= nil and #Rows == #Column.Rows then for I, V in ipairs(Rows) do Column.Rows[I].MaxH = Rows[I].RequestH end end end remove(Stack, 1) Active = nil if #Stack > 0 then Active = Stack[1] end end function LayoutManager.SameLine(CursorOptions) Cursor.SameLine(CursorOptions) if Active ~= nil then local Column = Active.Columns[Active.ColumnNo] Column.RowNo = max(Column.RowNo - 1, 1) if Column.Rows ~= nil and CursorOptions ~= nil then local Row = Column.Rows[Column.RowNo] local Pad = CursorOptions.Pad if Row ~= nil and Pad ~= nil then Row.CursorX = Row.CursorX + Pad Cursor.SetX(Row.CursorX) end end end end function LayoutManager.NewLine() if Active ~= nil then AddControl(Active, 0, Cursor.GetNewLineSize(), 'NewLine') end Cursor.NewLine() end function LayoutManager.SetColumn(Index) if Active ~= nil then Index = max(Index, 1) Index = min(Index, #Active.Columns) Active.ColumnNo = Index end end function LayoutManager.GetActiveSize() if Active ~= nil then return GetColumnSize(Active) end local WinX, WinY, WinW, WinH = GetWindowBounds() return WinW, WinH end function LayoutManager.GetCurrentColumnIndex() if Active ~= nil then return Active.ColumnNo end return 0 end function LayoutManager.Validate() local Message = nil for I, V in ipairs(Stack) do if Message == nil then Message = "The following layouts have not had EndLayout called:\n" end Message = Message .. "'" .. V.Id .. "' in window '" .. V.WindowId .. "'\n" end assert(Message == nil, Message) end --[[ This function will return a map of table names with their debug information. --]] function LayoutManager.GetDebugInfo() local Result = {} for K, V in pairs(Instances) do local Info = {} insert(Info, "X: " .. V.X) insert(Info, "Y: " .. V.Y) insert(Info, "AlignX: " .. V.AlignX) insert(Info, "AlignY: " .. V.AlignY) insert(Info, "AlignRowY: " .. V.AlignRowY) insert(Info, "Ignore: " .. tostring(V.Ignore)) insert(Info, "ExpandW: " .. tostring(V.ExpandW)) insert(Info, "ExpandH: " .. tostring(V.ExpandH)) insert(Info, "Columns: " .. #V.Columns) for ColumnNo, Column in ipairs(V.Columns) do insert(Info, " " .. ColumnNo .. ": W: " .. Column.W .. " Rows: " .. (Column.Rows and #Column.Rows or 0)) if Column.Rows ~= nil then for RowNo, Row in ipairs(Column.Rows) do insert(Info, " " .. RowNo .. ": W: " .. Row.W .. " H: " .. Row.H .. " Controls: " .. (Row.Controls and #Row.Controls or 0)) if Row.Controls ~= nil then for ControlNo, Control in pairs(Row.Controls) do insert(Info, " " .. ControlNo .. ": " .. " W: " .. Control.W .. " H: " .. Control.H .. " Type: " .. tostring(Control.Type)) end end end end end Result[K] = Info end return Result end return LayoutManager ================================================ FILE: Internal/UI/ListBox.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local ListBox = {} local Instances = {} local ActiveInstance = nil local EMPTY = {} local IGNORE = { Ignore = true } local function GetItemInstance(instance, id) if not instance then return end if instance.Items[id] == nil then instance.Items[id] = { Id = id, X = 0, Y = 0, W = 0, H = 0, } end return instance.Items[id] end local function GetInstance(id) if Instances[id] == nil then Instances[id] = { Id = id, X = 0, Y = 0, W = 0, H = 0, Items = {}, ActiveItem = nil, HotItem = nil, Selected = false, StatHandle = nil, Region = { AutoSizeContent = true, Intersect = true, }, } end return Instances[id] end function ListBox.Begin(id, options) local statHandle = Stats.Begin('ListBox', 'Slab') options = options or EMPTY local w = options.W or 150 local h = options.H or 150 local rounding = options.Rounding or Style.WindowRounding local bgColor = options.BgColor or Style.ListBoxBgColor local instance = GetInstance(Window.GetItemId(id)) if options.Clear then for i = 1, #instance.Items do instance.Items[i] = nil end end w, h = LayoutManager.ComputeSize(w, h) LayoutManager.AddControl(w, h, 'ListBox') do local remainingW, remainingH = Window.GetRemainingSize() if options.StretchW then w = remainingW end if options.StretchH then h = remainingH end end local x, y = Cursor.GetPosition() instance.X = x instance.Y = y instance.W = w instance.H = h instance.StatHandle = statHandle ActiveInstance = instance Cursor.SetItemBounds(x, y, w, h) Cursor.AdvanceY(0) Window.AddItem(x, y, w, h, instance.Id) local isObstructed = Window.IsObstructedAtMouse() local mouseX, mouseY = Window.GetMousePosition() do local tx, ty = Window.TransformPoint(x, y) local region = instance.Region region.X = x region.Y = y region.W = w region.H = h region.SX = tx region.SY = ty region.BgColor = bgColor region.MouseX = mouseX region.MouseY = mouseY region.ResetContent = Window.HasResized() region.IsObstructed = isObstructed region.Rounding = rounding Region.Begin(instance.Id, region) end local in_region = Region.Contains(mouseX, mouseY) instance.HotItem = nil mouseX, mouseY = Region.InverseTransform(instance.Id, mouseX, mouseY) for k, v in pairs(instance.Items) do if not isObstructed and not Region.IsHoverScrollBar(instance.Id) and v.X <= mouseX and mouseX <= v.X + instance.W and v.Y <= mouseY and mouseY <= v.Y + v.H and in_region then instance.HotItem = v end if instance.HotItem == v or v.Selected then DrawCommands.Rectangle('fill', v.X, v.Y, instance.W, v.H, Style.TextHoverBgColor) end end LayoutManager.Begin('Ignore', IGNORE) end function ListBox.BeginItem(id, options) options = options or EMPTY assert(ActiveInstance ~= nil, "Trying to call BeginListBoxItem outside of BeginListBox.") if ActiveInstance.ActiveItem ~= nil then error(("BeginListBoxItem was called for item '%s' without a call to EndListBoxItem."):format( ActiveInstance.ActiveItem.Id or "nil" )) end local item = GetItemInstance(ActiveInstance, id) item.X = ActiveInstance.X item.Y = Cursor.GetY() Cursor.SetX(item.X) Cursor.AdvanceX(0) ActiveInstance.ActiveItem = item ActiveInstance.ActiveItem.Selected = options.Selected end function ListBox.IsItemClicked(button, isDoubleClick) assert(ActiveInstance ~= nil, "Trying to call IsItemClicked outside of BeginListBox.") assert(ActiveInstance.ActiveItem ~= nil, "IsItemClicked was called outside of BeginListBoxItem.") if ActiveInstance.HotItem ~= ActiveInstance.ActiveItem then return false end button = button or 1 if isDoubleClick then return Mouse.IsDoubleClicked(button) else return Mouse.IsClicked(button) end end function ListBox.EndItem() assert(ActiveInstance ~= nil, "Trying to call BeginListBoxItem outside of BeginListBox.") assert(ActiveInstance.ActiveItem ~= nil, "Trying to call EndListBoxItem without calling BeginListBoxItem.") local itemX, itemY, itemW, itemH = Cursor.GetItemBounds() ActiveInstance.ActiveItem.W = itemW ActiveInstance.ActiveItem.H = Cursor.GetLineHeight() Cursor.SetY(ActiveInstance.ActiveItem.Y + ActiveInstance.ActiveItem.H) Cursor.AdvanceY(0) ActiveInstance.ActiveItem = nil end function ListBox.End() assert(ActiveInstance ~= nil, "EndListBox was called without calling BeginListBox.") Region.End() Region.ApplyScissor() Cursor.SetItemBounds(ActiveInstance.X, ActiveInstance.Y, ActiveInstance.W, ActiveInstance.H) Cursor.SetPosition(ActiveInstance.X, ActiveInstance.Y) Cursor.AdvanceY(ActiveInstance.H) LayoutManager.End() Stats.End(ActiveInstance.StatHandle) ActiveInstance = nil end return ListBox ================================================ FILE: Internal/UI/Menu.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local remove = table.remove local max = math.max local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local MenuState = require(SLAB_PATH .. '.Internal.UI.MenuState') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local Menu = {} local Instances = {} local Pad = 8.0 local LeftPad = 25.0 local RightPad = 70.0 local CheckSize = 5.0 local OpenedContextMenu = nil local function IsItemHovered() local ItemX, ItemY, ItemW, ItemH = Cursor.GetItemBounds() local MouseX, MouseY = Window.GetMousePosition() return not Window.IsObstructedAtMouse() and ItemX < MouseX and MouseX < ItemX + Window.GetWidth() and ItemY < MouseY and MouseY < ItemY + ItemH end local function AlterOptions(Options) Options = Options == nil and {} or Options Options.Enabled = Options.Enabled == nil and true or Options.Enabled Options.IsSelectable = Options.Enabled Options.SelectOnHover = Options.Enabled Options.PadH = Style.MenuItemPadH if Options.Enabled then Options.Color = Style.TextColor else Options.Color = Style.TextDisabledColor end return Options end local function ConstrainPosition(X, Y, W, H) local ResultX, ResultY = X, Y local Right = X + W local Bottom = Y + H local OffsetX = Right >= Scale.GetScreenWidth() local OffsetY = Bottom >= Scale.GetScreenHeight() if OffsetX then ResultX = X - (Right - Scale.GetScreenWidth()) end if OffsetY then ResultY = Y - H end local WinX, WinY, WinW, WinH = Window.GetBounds() if OffsetX then ResultX = WinX - W end ResultX = max(ResultX, 0.0) ResultY = max(ResultY, 0.0) return ResultX, ResultY end local function BeginWindow(Id, X, Y, RoundAllCorners) local Instance = Instances[Id] if Instance ~= nil then X, Y = ConstrainPosition(X, Y, Instance.W, Instance.H) end Cursor.PushContext() Window.Begin(Id, { X = X, Y = Y, W = 0.0, H = 0.0, AllowResize = false, AllowFocus = false, Border = 0.0, AutoSizeWindow = true, Layer = 'ContextMenu', BgColor = Style.MenuColor, Rounding = RoundAllCorners and {2, 2, 2, 2} or {0, 0, 2, 2}, NoSavedSettings = true }) end function Menu.BeginMenu(Label, Options) local Result = false local X, Y = Cursor.GetPosition() local IsMenuBar = Window.IsMenuBar() local Id = Window.GetId() .. "." .. Label local Win = Window.Top() Options = AlterOptions(Options) Options.IsSelected = Options.Enabled and Win.Selected == Id if IsMenuBar then Options.IsSelectableTextOnly = Options.Enabled Options.Pad = Pad * 2 Options.PadH = Style.MenuPadH else Cursor.SetX(X + LeftPad) end local MenuX = 0.0 local MenuY = 0.0 -- 'Result' may be false if 'Enabled' is false. The hovered state is still required -- so that will be handled differently. Result = Text.Begin(Label, Options) local ItemX, ItemY, ItemW, ItemH = Cursor.GetItemBounds() if IsMenuBar then Cursor.SameLine() -- Menubar items don't extend to the width of the window since these items are layed out horizontally. Only -- need to perform hover check on item bounds. local Hovered = Cursor.IsInItemBounds(Window.GetMousePosition()) if Hovered then if Mouse.IsClicked(1) then if Result then MenuState.WasOpened = MenuState.IsOpened MenuState.IsOpened = not MenuState.IsOpened if MenuState.IsOpened then MenuState.RequestClose = false end elseif MenuState.WasOpened then MenuState.RequestClose = false end end end if MenuState.IsOpened and OpenedContextMenu == nil then if Result then Win.Selected = Id end else Win.Selected = nil end MenuX = X MenuY = Y + Window.GetHeight() else local WinX, WinY, WinW, WinH = Window.GetBounds() local H = Style.Font:getHeight() local TriX = WinX + WinW - H * 0.75 local TriY = Y + H * 0.5 local Radius = H * 0.35 DrawCommands.Triangle('fill', TriX, TriY, Radius, 90, Style.TextColor) MenuX = X + WinW MenuY = Y if Result then Win.Selected = Id end Window.AddItem(ItemX, ItemY, ItemW + RightPad, ItemH) -- Prevent closing the menu window if this item is clicked. if IsItemHovered() and Mouse.IsClicked(1) then MenuState.RequestClose = false end end Result = Win.Selected == Id if Result then BeginWindow(Id, MenuX, MenuY, not IsMenuBar) end return Result end function Menu.MenuItem(Label, Options) Options = AlterOptions(Options) local HintWidth = Options.Hint == nil and 0 or Text.GetWidth(Options.Hint) Cursor.SetX(Cursor.GetX() + LeftPad) local Result = Text.Begin(Label, Options) local ItemX, ItemY, ItemW, ItemH = Cursor.GetItemBounds() Window.AddItem(ItemX, ItemY, ItemW + RightPad + HintWidth, ItemH) if Options.Hint ~= nil then Cursor.SameLine() Text.BeginFormatted(Options.Hint, { Align = "right", W = Window.GetRemainingSize() - LeftPad, -- Pad the right side equal to the left side H = ItemH, Color = Style.TextDisabledColor, RightPad = LeftPad, -- hack, see Text.BeginFormatted() }) end if Result then local Win = Window.Top() Win.Selected = nil Result = Mouse.IsClicked(1) if Result and MenuState.WasOpened then MenuState.RequestClose = true end else if IsItemHovered() and Mouse.IsClicked(1) then MenuState.RequestClose = false end end return Result end function Menu.MenuItemChecked(Label, IsChecked, Options) Options = AlterOptions(Options) local X, Y = Cursor.GetPosition() local Result = Menu.MenuItem(Label, Options) if IsChecked then local H = Style.Font:getHeight() + Options.PadH DrawCommands.Check(X + LeftPad * 0.5, Y + H * 0.5, CheckSize, Options.Color) end return Result end function Menu.Separator() local Ctx = Context.Top() if Ctx.Type == 'Menu' then local Item = GetItem("Sep_" .. Ctx.Data.SeparatorId) Item.IsSeparator = true Ctx.Data.SeparatorId = Ctx.Data.SeparatorId + 1 end end function Menu.EndMenu() local Id = Window.GetId() if Instances[Id] == nil then Instances[Id] = {} end Instances[Id].W = Window.GetWidth() Instances[Id].H = Window.GetHeight() Window.End() Cursor.PopContext() end function Menu.Pad() return Pad end function Menu.BeginContextMenu(Options) Options = Options == nil and {} or Options Options.IsItem = Options.IsItem == nil and false or Options.IsItem Options.IsWindow = Options.IsWindow == nil and false or Options.IsWindow Options.Button = Options.Button == nil and 2 or Options.Button local BaseId = nil local Id = nil if Options.IsWindow then BaseId = Window.GetId() elseif Options.IsItem then BaseId = Window.GetContextHotItem() if BaseId == nil then BaseId = Window.GetHotItem() end end if Options.IsItem and Window.GetLastItem() ~= BaseId then return false end if BaseId ~= nil then Id = BaseId .. '.ContextMenu' end if Id == nil then return false end if MenuState.IsOpened and OpenedContextMenu ~= nil then if OpenedContextMenu.Id == Id then BeginWindow(OpenedContextMenu.Id, OpenedContextMenu.X, OpenedContextMenu.Y, true) return true end return false end local IsOpening = false if not Window.IsObstructedAtMouse() and Window.IsMouseHovered() and Mouse.IsClicked(Options.Button) then local IsValidWindow = Options.IsWindow and Window.GetHotItem() == nil local IsValidItem = Options.IsItem if IsValidWindow or IsValidItem then MenuState.IsOpened = true IsOpening = true end end if IsOpening then local X, Y = Mouse.Position() X, Y = ConstrainPosition(X, Y, 0.0, 0.0) OpenedContextMenu = {Id = Id, X = X, Y = Y, Win = Window.Top()} Window.SetContextHotItem(Options.IsItem and BaseId or nil) end return false end function Menu.EndContextMenu() Menu.EndMenu() end function Menu.Close() MenuState.WasOpened = MenuState.IsOpened MenuState.IsOpened = false MenuState.RequestClose = false if OpenedContextMenu ~= nil then OpenedContextMenu.Win.ContextHotItem = nil OpenedContextMenu = nil end end return Menu ================================================ FILE: Internal/UI/MenuBar.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Cursor = require(SLAB_PATH .. ".Internal.Core.Cursor") local DrawCommands = require(SLAB_PATH .. ".Internal.Core.DrawCommands") local Menu = require(SLAB_PATH .. ".Internal.UI.Menu") local MenuState = require(SLAB_PATH .. ".Internal.UI.MenuState") local Style = require(SLAB_PATH .. ".Style") local Window = require(SLAB_PATH .. ".Internal.UI.Window") local MenuBar = {} local Instances = {} local function GetInstance() local Win = Window.Top() if Instances[Win] == nil then local Instance = {} Instance.Selected = nil Instance.Id = Win.Id .. '_MenuBar' Instances[Win] = Instance end return Instances[Win] end function MenuBar.Begin(IsMainMenuBar) local X, Y = Cursor.GetPosition() local WinX, WinY, WinW, WinH = Window.GetBounds() local Instance = GetInstance() if not MenuState.IsOpened then Instance.Selected = nil end if IsMainMenuBar then MenuState.MainMenuBarH = Style.Font:getHeight() + Style.MenuPadH end Window.Begin(Instance.Id, { X = X, Y = Y, W = WinW, H = Style.Font:getHeight() + Style.MenuPadH, AllowResize = false, AllowFocus = false, Border = 0.0, BgColor = Style.MenuColor, NoOutline = true, IsMenuBar = true, AutoSizeWindow = false, AutoSizeContent = false, Layer = IsMainMenuBar and 'MainMenuBar' or nil, Rounding = 0.0, NoSavedSettings = true }) Cursor.AdvanceX(4.0) return true end function MenuBar.End() Window.End() end function MenuBar.Clear() for I, V in ipairs(Instances) do V.Selected = nil end end return MenuBar ================================================ FILE: Internal/UI/MenuState.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local MenuState = { IsOpened = false, WasOpened = false, RequestClose = false, MainMenuBarH = 0 } return MenuState ================================================ FILE: Internal/UI/Region.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local max = math.max local min = math.min local floor = math.floor local DrawCommands = require(SLAB_PATH .. ".Internal.Core.DrawCommands") local MenuState = require(SLAB_PATH .. ".Internal.UI.MenuState") local Mouse = require(SLAB_PATH .. ".Internal.Input.Mouse") local Style = require(SLAB_PATH .. ".Style") local Utility = require(SLAB_PATH .. ".Internal.Core.Utility") local Region = {} local Instances = {} local Stack = {} local ActiveInstance = nil local ScrollPad = 3 local ScrollBarSize = 10 local WheelX = 0 local WheelY = 0 local WheelSpeed = 3 local HotInstance = nil local WheelInstance = nil local ScrollInstance = nil local EMPTY = {} local tempColor = {} local function GetXScrollSize(instance) if instance ~= nil then return max(instance.W - (instance.ContentW - instance.W), 10) end return 0 end local function GetYScrollSize(instance) if instance ~= nil then return max(instance.H - (instance.ContentH - instance.H), 10) end return 0 end local function IsScrollHovered(instance, x, y) local hasScrollX, hasScrollY = false, false if not instance then return end if instance.HasScrollX then local posY = instance.Y + instance.H - ScrollPad - ScrollBarSize local sizeX = GetXScrollSize(instance) local posX = instance.ScrollPosX hasScrollX = instance.X + posX <= x and x < instance.X + posX + sizeX and posY <= y and y < posY + ScrollBarSize end if instance.HasScrollY then local posX = instance.X + instance.W - ScrollPad - ScrollBarSize local sizeY = GetYScrollSize(instance) local posY = instance.ScrollPosY hasScrollY = posX <= x and x < posX + ScrollBarSize and instance.Y + posY <= y and y < instance.Y + posY + sizeY end return hasScrollX, hasScrollY end local function Contains(instance, x, y) if instance ~= nil then return instance.X <= x and x <= instance.X + instance.W and instance.Y <= y and y <= instance.Y + instance.H end return false end local function UpdateScrollBars(instance, isObstructed, forceShowX, forceShowY) if instance.IgnoreScroll then return end instance.HasScrollX = forceShowX or (instance.ContentW > instance.W) instance.HasScrollY = forceShowY or (instance.ContentH > instance.H) local x, y = instance.MouseX, instance.MouseY instance.HoverScrollX, instance.HoverScrollY = IsScrollHovered(instance, x, y) local xSize = instance.W - GetXScrollSize(instance) local ySize = instance.H - GetYScrollSize(instance) if isObstructed then instance.HoverScrollX = false instance.HoverScrollY = false end local isMouseReleased = Mouse.IsReleased(1) local isMouseClicked = Mouse.IsClicked(1) local deltaX, deltaY = Mouse.GetDelta() if WheelInstance == instance then instance.HoverScrollX = WheelX ~= 0 instance.HoverScrollY = WheelY ~= 0 if not instance.HoverScrollX and instance.HoverScrollY and not instance.HasScrollY then instance.HoverScrollX = instance.HoverScrollY WheelX = -WheelY end end if not isObstructed and Contains(instance, x, y) or (instance.HoverScrollX or instance.HoverScrollY) then if WheelInstance == instance then if WheelX ~= 0 then instance.ScrollPosX = max(instance.ScrollPosX + WheelX, 0) instance.IsScrollingX = true isMouseReleased = true WheelX = 0 end if WheelY ~= 0 then instance.ScrollPosY = max(instance.ScrollPosY - WheelY, 0) instance.IsScrollingY = true isMouseReleased = true WheelY = 0 end WheelInstance = nil ScrollInstance = instance end if ScrollInstance == nil and isMouseClicked and (instance.HoverScrollX or instance.HoverScrollY) then ScrollInstance = instance ScrollInstance.IsScrollingX = instance.HoverScrollX ScrollInstance.IsScrollingY = instance.HoverScrollY end end if ScrollInstance == instance and isMouseReleased then instance.IsScrollingX = false instance.IsScrollingY = false ScrollInstance = nil end if instance.HasScrollX then if instance.HasScrollY then xSize = xSize - ScrollBarSize - ScrollPad end xSize = max(xSize, 0) if ScrollInstance == instance then MenuState.RequestClose = false if instance.IsScrollingX then instance.ScrollPosX = max(instance.ScrollPosX + deltaX, 0) end end instance.ScrollPosX = min(instance.ScrollPosX, xSize) end if instance.HasScrollY then if instance.HasScrollX then ySize = ySize - ScrollBarSize - ScrollPad end ySize = max(ySize, 0) if ScrollInstance == instance then MenuState.RequestClose = false if instance.IsScrollingY then instance.ScrollPosY = max(instance.ScrollPosY + deltaY, 0) end end instance.ScrollPosY = min(instance.ScrollPosY, ySize) end local xRatio, yRatio = 0, 0 if xSize ~= 0 then xRatio = max(instance.ScrollPosX / xSize, 0) end if ySize ~= 0 then yRatio = max(instance.ScrollPosY / ySize, 0) end local tx = max(instance.ContentW - instance.W, 0) * -xRatio local ty = max(instance.ContentH - instance.H, 0) * -yRatio instance.Transform:setTransformation(floor(tx), floor(ty)) end local function DrawScrollBars(instance) if not instance.HasScrollX and not instance.HasScrollY then return end if HotInstance ~= instance and ScrollInstance ~= instance and not Utility.IsMobile() then local dt = love.timer.getDelta() instance.ScrollAlphaX = max(instance.ScrollAlphaX - dt, 0) instance.ScrollAlphaY = max(instance.ScrollAlphaY - dt, 0) else instance.ScrollAlphaX = 1 instance.ScrollAlphaY = 1 end if instance.HasScrollX then local xSize = GetXScrollSize(instance) local color = Utility.MakeColor(Style.ScrollBarColor, tempColor) if instance.HoverScrollX or instance.IsScrollingX then color = Utility.MakeColor(Style.ScrollBarHoveredColor, tempColor) end color[4] = instance.ScrollAlphaX local xPos = instance.ScrollPosX DrawCommands.Rectangle('fill', instance.X + xPos, instance.Y + instance.H - ScrollPad - ScrollBarSize, xSize, ScrollBarSize, color, Style.ScrollBarRounding) end if instance.HasScrollY then local ySize = GetYScrollSize(instance) local color = Utility.MakeColor(Style.ScrollBarColor, tempColor) if instance.HoverScrollY or instance.IsScrollingY then color = Utility.MakeColor(Style.ScrollBarHoveredColor, tempColor) end color[4] = instance.ScrollAlphaY local yPos = instance.ScrollPosY DrawCommands.Rectangle('fill', instance.X + instance.W - ScrollPad - ScrollBarSize, instance.Y + yPos, ScrollBarSize, ySize, color, Style.ScrollBarRounding) end end local function GetInstance(id) if id == nil then return ActiveInstance end if Instances[id] == nil then local instance = { Id = id, X = 0, Y = 0, W = 0, H = 0, SX = 0, SY = 0, ContentW = 0, ContentH = 0, HasScrollX = false, HasScrollY = false, HoverScrollX = false, HoverScrollY = false, IsScrollingX = false, IsScrollingY = false, ScrollPosX = 0, ScrollPosY = 0, ScrollAlphaX = 0, ScrollAlphaY = 0, Intersect = false, AutoSizeContent = false, Transform = love.math.newTransform(), } Instances[id] = instance end return Instances[id] end function Region.Begin(id, options) options = options or EMPTY local instance = GetInstance(id) instance.X = options.X or 0 instance.Y = options.Y or 0 instance.W = options.W or 0 instance.H = options.H or 0 instance.SX = options.SX or instance.X instance.SY = options.SY or instance.Y instance.Intersect = options.Intersect instance.IgnoreScroll = options.IgnoreScroll instance.MouseX = options.MouseX or 0 instance.MouseY = options.MouseY or 0 instance.AutoSizeContent = options.AutoSizeContent if options.ResetContent then instance.ContentW = 0 instance.ContentH = 0 end if not options.AutoSizeContent then instance.ContentW = options.ContentW or 0 instance.ContentH = options.ContentH or 0 end ActiveInstance = instance table.insert(Stack, 1, ActiveInstance) UpdateScrollBars(instance, options.IsObstructed, options.ForceShowX, options.ForceShowY) if options.AutoSizeContent then instance.ContentH = 0 instance.ContentW = 0 end if HotInstance == instance and (not Contains(instance, instance.MouseX, instance.MouseY) or options.IsObstructed) then HotInstance = nil end if not options.IsObstructed then if ( options.ForceShowX or options.ForceShowY or Contains(instance, instance.MouseX, instance.MouseY) or (instance.HoverScrollX or instance.HoverScrollY) ) then if ScrollInstance == nil then HotInstance = instance else HotInstance = ScrollInstance end end end if not options.NoBackground then DrawCommands.Rectangle('fill', instance.X, instance.Y, instance.W, instance.H, options.BgColor or Style.WindowBackgroundColor, options.Rounding or 0) end if not options.NoOutline then DrawCommands.Rectangle('line', instance.X, instance.Y, instance.W, instance.H, nil, options.Rounding or 0) end DrawCommands.TransformPush() DrawCommands.ApplyTransform(instance.Transform) Region.ApplyScissor() end function Region.End() DrawCommands.TransformPop() DrawScrollBars(ActiveInstance) if HotInstance == ActiveInstance and WheelInstance == nil and (WheelX ~= 0 or WheelY ~= 0) and not ActiveInstance.IgnoreScroll then WheelInstance = ActiveInstance end if ActiveInstance.Intersect then DrawCommands.IntersectScissor() else DrawCommands.Scissor() end ActiveInstance = nil table.remove(Stack, 1) if #Stack > 0 then ActiveInstance = Stack[1] end end function Region.IsHoverScrollBar(id) local instance = GetInstance(id) if instance ~= nil then return instance.HoverScrollX or instance.HoverScrollY end return false end function Region.Translate(id, x, y) local instance = GetInstance(id) if instance ~= nil then instance.Transform:translate(x, y) local tx, ty = instance.Transform:inverseTransformPoint(0, 0) if not instance.IgnoreScroll then if x ~= 0 and instance.HasScrollX then local xSize = instance.W - GetXScrollSize(instance) local ContentW = instance.ContentW - instance.W if instance.HasScrollY then xSize = xSize - ScrollPad - ScrollBarSize end xSize = max(xSize, 0) instance.ScrollPosX = (tx / ContentW) * xSize instance.ScrollPosX = max(instance.ScrollPosX, 0) instance.ScrollPosX = min(instance.ScrollPosX, xSize) end if y ~= 0 and instance.HasScrollY then local ySize = instance.H - GetYScrollSize(instance) if instance.HasScrollX then ySize = ySize - ScrollPad - ScrollBarSize end ySize = max(ySize, 0) local ContentH = instance.ContentH - instance.H instance.ScrollPosY = (ty / ContentH) * ySize instance.ScrollPosY = max(instance.ScrollPosY, 0) instance.ScrollPosY = min(instance.ScrollPosY, ySize) end end end end function Region.Transform(id, x, y) local instance = GetInstance(id) if instance ~= nil then return instance.Transform:transformPoint(x, y) end return x, y end function Region.InverseTransform(id, x, y) local instance = GetInstance(id) if instance ~= nil then return instance.Transform:inverseTransformPoint(x, y) end return x, y end function Region.ResetTransform(id) local instance = GetInstance(id) if instance ~= nil then instance.Transform:reset() instance.ScrollPosX = 0 instance.ScrollPosY = 0 end end function Region.IsActive(id) if ActiveInstance ~= nil then return ActiveInstance.Id == id end return false end function Region.AddItem(x, y, W, H) if ActiveInstance ~= nil and ActiveInstance.AutoSizeContent then local newW = x + W - ActiveInstance.X local newH = y + H - ActiveInstance.Y ActiveInstance.ContentW = max(ActiveInstance.ContentW, newW) ActiveInstance.ContentH = max(ActiveInstance.ContentH, newH) end end function Region.ApplyScissor() if ActiveInstance ~= nil then if ActiveInstance.Intersect then DrawCommands.IntersectScissor(ActiveInstance.SX, ActiveInstance.SY, ActiveInstance.W, ActiveInstance.H) else DrawCommands.Scissor(ActiveInstance.SX, ActiveInstance.SY, ActiveInstance.W, ActiveInstance.H) end end end function Region.GetBounds() if ActiveInstance ~= nil then return ActiveInstance.X, ActiveInstance.Y, ActiveInstance.W, ActiveInstance.H end return 0, 0, 0, 0 end function Region.GetContentSize() if ActiveInstance ~= nil then return ActiveInstance.ContentW, ActiveInstance.ContentH end return 0, 0 end function Region.GetContentBounds() if ActiveInstance ~= nil then return ActiveInstance.X, ActiveInstance.Y, ActiveInstance.ContentW, ActiveInstance.ContentH end return 0, 0, 0, 0 end function Region.Contains(x, y) if ActiveInstance ~= nil then return ActiveInstance.X <= x and x <= ActiveInstance.X + ActiveInstance.W and ActiveInstance.Y <= y and y <= ActiveInstance.Y + ActiveInstance.H end return false end function Region.ResetContentSize(id) local instance = GetInstance(id) if instance ~= nil then instance.ContentW = 0 instance.ContentH = 0 end end function Region.GetScrollPad() return ScrollPad end function Region.GetScrollBarSize() return ScrollBarSize end function Region.WheelMoved(x, y) WheelX = x * WheelSpeed WheelY = y * WheelSpeed end function Region.GetWheelDelta() return WheelX, WheelY end function Region.IsScrolling(id) if id then local instance = GetInstance(id) return ScrollInstance == instance or WheelInstance == instance end if ScrollInstance then return ScrollInstance.IsScrollingX or ScrollInstance.IsScrollingY end if WheelInstance then return WheelInstance.IsScrollingX or WheelInstance.IsScrollingY end return false end function Region.GetHotInstanceId() return HotInstance and HotInstance.Id or '' end function Region.ClearHotInstance(id) if not id or (HotInstance and HotInstance.Id == id) then HotInstance = nil end end function Region.GetInstanceIds() local result = {} for k, v in pairs(Instances) do table.insert(result, k) end return result end function Region.GetDebugInfo(id) local result = {} local instance = Instances[id] table.insert(result, "ScrollInstance: " .. (ScrollInstance ~= nil and ScrollInstance.Id or "nil")) table.insert(result, "WheelInstance: " .. (WheelInstance ~= nil and WheelInstance.Id or "nil")) table.insert(result, "WheelX: " .. WheelX) table.insert(result, "WheelY: " .. WheelY) table.insert(result, "Wheel Speed: " .. WheelSpeed) if instance ~= nil then table.insert(result, "Id: " .. instance.Id) table.insert(result, "W: " .. instance.W) table.insert(result, "H: " .. instance.H) table.insert(result, "ContentW: " .. instance.ContentW) table.insert(result, "ContentH: " .. instance.ContentH) table.insert(result, "ScrollPosX: " .. instance.ScrollPosX) table.insert(result, "ScrollPosY: " .. instance.ScrollPosY) local tx, ty = instance.Transform:transformPoint(0, 0) table.insert(result, "TX: " .. tx) table.insert(result, "TY: " .. ty) table.insert(result, "Max TX: " .. instance.ContentW - instance.W) table.insert(result, "Max TY: " .. instance.ContentH - instance.H) end return result end function Region.SetWheelSpeed(Speed) WheelSpeed = Speed or 3 end function Region.GetWheelSpeed() return WheelSpeed end return Region ================================================ FILE: Internal/UI/Separator.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local max = math.max local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Style = require(SLAB_PATH .. '.Style') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Separator = {} function Separator.Begin(Options) Options = Options == nil and {} or Options Options.IncludeBorders = Options.IncludeBorders == nil and false or Options.IncludeBorders Options.H = Options.H == nil and 4.0 or Options.H Options.Thickness = Options.Thickness == nil and 1.0 or Options.Thickness local W, H = LayoutManager.GetActiveSize() W, H = LayoutManager.ComputeSize(W, max(Options.H, Options.Thickness)) LayoutManager.AddControl(W, H, 'Separator') local X, Y = Cursor.GetPosition() if Options.IncludeBorders then W = W + Window.GetBorder() * 2 X = X - Window.GetBorder() end DrawCommands.Line(X, Y + H * 0.5, X + W, Y + H * 0.5, Options.Thickness, Style.SeparatorColor) Cursor.SetItemBounds(X, Y, W, H) Cursor.AdvanceY(H) end return Separator ================================================ FILE: Internal/UI/Shape.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local abs = math.abs local max = math.max local min = math.min local huge = math.huge local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Shape = {} local Curve = nil local CurveX, CurveY = 0, 0 function Shape.Rectangle(Options) local StatHandle = Stats.Begin('Rectangle', 'Slab') Options = Options == nil and {} or Options Options.Mode = Options.Mode == nil and 'fill' or Options.Mode Options.W = Options.W == nil and 32 or Options.W Options.H = Options.H == nil and 32 or Options.H Options.Color = Options.Color == nil and nil or Options.Color Options.Rounding = Options.Rounding == nil and 2.0 or Options.Rounding Options.Outline = Options.Outline == nil and false or Options.Outline Options.OutlineColor = Options.OutlineColor == nil and {0.0, 0.0, 0.0, 1.0} or Options.OutlineColor Options.Segments = Options.Segments == nil and 10 or Options.Segments local W = Options.W local H = Options.H LayoutManager.AddControl(W, H, 'Rectangle') local X, Y = Cursor.GetPosition() if Options.Outline then DrawCommands.Rectangle('line', X, Y, W, H, Options.OutlineColor, Options.Rounding, Options.Segments) end DrawCommands.Rectangle(Options.Mode, X, Y, W, H, Options.Color, Options.Rounding, Options.Segments) Window.AddItem(X, Y, W, H) Cursor.SetItemBounds(X, Y, W, H) Cursor.AdvanceY(H) Stats.End(StatHandle) end function Shape.Circle(Options) local StatHandle = Stats.Begin('Circle', 'Slab') Options = Options == nil and {} or Options Options.Mode = Options.Mode == nil and 'fill' or Options.Mode Options.Radius = Options.Radius == nil and 12.0 or Options.Radius Options.Color = Options.Color == nil and nil or Options.Color Options.Segments = Options.Segments == nil and nil or Options.Segments local Diameter = Options.Radius * 2.0 LayoutManager.AddControl(Diameter, Diameter, 'Circle') local X, Y = Cursor.GetPosition() local CenterX = X + Options.Radius local CenterY = Y + Options.Radius DrawCommands.Circle(Options.Mode, CenterX, CenterY, Options.Radius, Options.Color, Options.Segments) Window.AddItem(X, Y, Diameter, Diameter) Cursor.SetItemBounds(X, Y, Diameter, Diameter) Cursor.AdvanceY(Diameter) Stats.End(StatHandle) end function Shape.Triangle(Options) local StatHandle = Stats.Begin('Triangle', 'Slab') Options = Options == nil and {} or Options Options.Mode = Options.Mode == nil and 'fill' or Options.Mode Options.Radius = Options.Radius == nil and 12 or Options.Radius Options.Rotation = Options.Rotation == nil and 0 or Options.Rotation Options.Color = Options.Color == nil and nil or Options.Color local Diameter = Options.Radius * 2.0 LayoutManager.AddControl(Diameter, Diameter, 'Triangle') local X, Y = Cursor.GetPosition() local CenterX = X + Options.Radius local CenterY = Y + Options.Radius DrawCommands.Triangle(Options.Mode, CenterX, CenterY, Options.Radius, Options.Rotation, Options.Color) Window.AddItem(X, Y, Diameter, Diameter) Cursor.SetItemBounds(X, Y, Diameter, Diameter) Cursor.AdvanceY(Diameter) Stats.End(StatHandle) end function Shape.Line(X2, Y2, Options) local StatHandle = Stats.Begin('Line', 'Slab') Options = Options == nil and {} or Options Options.Width = Options.Width == nil and 1.0 or Options.Width Options.Color = Options.Color == nil and nil or Options.Color local X, Y = Cursor.GetPosition() local W, H = abs(X2 - X), abs(Y2 - Y) H = max(H, Options.Width) DrawCommands.Line(X, Y, X2, Y2, Options.Width, Options.Color) Window.AddItem(X, Y, W, H) Cursor.SetItemBounds(X, Y, W, H) Cursor.AdvanceY(H) Stats.End(StatHandle) end function Shape.Curve(Points, Options) local StatHandle = Stats.Begin('Curve', 'Slab') Options = Options == nil and {} or Options Options.Color = Options.Color == nil and nil or Options.Color Options.Depth = Options.Depth == nil and nil or Options.Depth Curve = love.math.newBezierCurve(Points) local MinX, MinY = huge, huge local MaxX, MaxY = 0, 0 for I = 1, Curve:getControlPointCount(), 1 do local PX, PY = Curve:getControlPoint(I) MinX = min(MinX, PX) MinY = min(MinY, PY) MaxX = max(MaxX, PX) MaxY = max(MaxY, PY) end local W = abs(MaxX - MinX) local H = abs(MaxY - MinY) LayoutManager.AddControl(W, H, 'Curve') CurveX, CurveY = Cursor.GetPosition() Curve:translate(CurveX, CurveY) DrawCommands.Curve(Curve:render(Options.Depth), Options.Color) Window.AddItem(MinX, MinY, W, H) Cursor.SetItemBounds(MinX, MinY, W, H) Cursor.AdvanceY(H) Stats.End(StatHandle) end function Shape.GetCurveControlPointCount() if Curve ~= nil then return Curve:getControlPointCount() end return 0 end function Shape.GetCurveControlPoint(Index, Options) Options = Options == nil and {} or Options Options.LocalSpace = Options.LocalSpace == nil and true or Options.LocalSpace local X, Y = 0, 0 if Curve ~= nil then if Options.LocalSpace then Curve:translate(-CurveX, -CurveY) end X, Y = Curve:getControlPoint(Index) if Options.LocalSpace then Curve:translate(CurveX, CurveY) end end return X, Y end function Shape.EvaluateCurve(Time, Options) Options = Options == nil and {} or Options Options.LocalSpace = Options.LocalSpace == nil and true or Options.LocalSpace local X, Y = 0, 0 if Curve ~= nil then if Options.LocalSpace then Curve:translate(-CurveX, -CurveY) end X, Y = Curve:evaluate(Time) if Options.LocalSpace then Curve:translate(CurveX, CurveY) end end return X, Y end function Shape.Polygon(Points, Options) local StatHandle = Stats.Begin('Polygon', 'Slab') Options = Options == nil and {} or Options Options.Color = Options.Color == nil and nil or Options.Color Options.Mode = Options.Mode == nil and 'fill' or Options.Mode local MinX, MinY = huge, huge local MaxX, MaxY = 0, 0 local Verts = {} for I = 1, #Points, 2 do MinX = min(MinX, Points[I]) MinY = min(MinY, Points[I+1]) MaxX = max(MaxX, Points[I]) MaxY = max(MaxY, Points[I+1]) end local W = abs(MaxX - MinX) local H = abs(MaxY - MinY) LayoutManager.AddControl(W, H, 'Polygon') MinX, MinY = huge, huge MaxX, MaxY = 0, 0 local X, Y = Cursor.GetPosition() for I = 1, #Points, 2 do insert(Verts, Points[I] + X) insert(Verts, Points[I+1] + Y) MinX = min(MinX, Verts[I]) MinY = min(MinY, Verts[I+1]) MaxX = max(MaxX, Verts[I]) MaxY = max(MaxY, Verts[I+1]) end DrawCommands.Polygon(Options.Mode, Verts, Options.Color) Window.AddItem(MinX, MinY, W, H) Cursor.SetItemBounds(MinX, MinY, W, H) Cursor.AdvanceY(H) Stats.End(StatHandle) end return Shape ================================================ FILE: Internal/UI/Text.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local floor = math.floor local insert = table.insert local max = math.max local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local Text = {} local EMPTY = {} function Text.Begin(label, options) local statHandle = Stats.Begin('Text', 'Slab') options = options or EMPTY local color = options.Color or Style.TextColor local pad = options.Pad or 0 -- TODO: rename on next major version? local padH = options.PadH or 0 local isSelectableTextOnly = options.IsSelectableTextOnly local isSelectable = options.IsSelectable or isSelectableTextOnly if options.URL ~= nil then isSelectableTextOnly = true color = Style.TextURLColor end local w = Text.GetWidth(label) local h = Style.Font:getHeight() LayoutManager.AddControl(w + pad, h + padH, 'Text') local result = false local winId = Window.GetItemId(label) local x, y = Cursor.GetPosition() local mouseX, mouseY = Window.GetMousePosition() local isObstructed = Window.IsObstructedAtMouse() if not isObstructed and x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then Window.SetHotItem(winId) end local winX, winY, winW, winH = Region.GetContentBounds() local checkX = isSelectableTextOnly and x or winX -- The region's width may have been reset prior to the first control being added. Account for this discrepency. local checkW = isSelectableTextOnly and w or max(winW, w) local hovered = not isObstructed and checkX <= mouseX and mouseX <= checkX + checkW + pad and y <= mouseY and mouseY <= y + h + padH if isSelectable or options.IsSelected then if hovered or options.IsSelected then DrawCommands.Rectangle('fill', checkX, y, checkW + pad, h + padH, options.HoverColor or Style.TextHoverBgColor) end result = hovered and (options.SelectOnHover or Mouse.IsClicked(1)) end if hovered and options.URL ~= nil then Mouse.SetCursor('hand') if Mouse.IsClicked(1) then love.system.openURL(options.URL) end end DrawCommands.Print(label, floor(x + pad * 0.5), floor(y + padH * 0.5), color, Style.Font) if options.URL ~= nil then DrawCommands.Line(x + pad, y + h, x + w, y + h, 1.0, color) end Cursor.SetItemBounds(x, y, w + pad, h + padH) Cursor.AdvanceY(h + padH) if options.AddItem ~= false then Window.AddItem(x, y, w + pad, h + padH, winId) end Stats.End(statHandle) return result end function Text.BeginFormatted(label, options) local statHandle = Stats.Begin('Textf', 'Slab') local winW, winH = Window.GetBorderlessSize() options = options or EMPTY local w = options.W or winW local h = options.H or 0 -- TODO: Hack to ensure right-aligned menu hints don't change menu item click area local rightPad = options.RightPad or 0 if Window.IsAutoSize() and options.W == nil then w = Scale.GetScreenWidth() end local width, wrapped = Style.Font:getWrap(label, w) local textHeight = #wrapped * Style.Font:getHeight() local height = max(h, textHeight) local padH = height - textHeight if options.W ~= nil then width = options.W end LayoutManager.AddControl(width, height, 'TextFormatted') local x, y = Cursor.GetPosition() DrawCommands.Printf(label, floor(x), floor(y + padH * 0.5), width, options.Align or 'left', options.Color or Style.TextColor, Style.Font) Cursor.SetItemBounds(floor(x), floor(y), width, height) Cursor.AdvanceY(height) Window.ResetContentSize() Window.AddItem(floor(x), floor(y), width + rightPad, height) Stats.End(statHandle) end function Text.BeginObject(object, options) local statHandle = Stats.Begin('TextObject', 'Slab') local winW, winH = Window.GetBorderlessSize() options = options or EMPTY options.Color = options.Color == nil and Style.TextColor or options.Color local w, h = object:getDimensions() LayoutManager.AddControl(w, h, 'TextObject') local x, y = Cursor.GetPosition() DrawCommands.Text(object, floor(x), floor(y), options.Color) Cursor.SetItemBounds(floor(x), floor(y), w, h) Cursor.AdvanceY(y) Window.ResetContentSize() Window.AddItem(floor(x), floor(y), w, h) Stats.End(statHandle) end function Text.GetWidth(label) return Style.Font:getWidth(label) end function Text.GetHeight() return Style.Font:getHeight() end function Text.GetSize(label) return Style.Font:getWidth(label), Style.Font:getHeight() end function Text.GetSizeWrap(label, width) local w, lines = Style.Font:getWrap(label, width) return w, #lines * Text.GetHeight() end function Text.GetLines(label, width) local _, lines = Style.Font:getWrap(label, width) local start = 0 for i, v in ipairs(lines) do local line local len_v = #v if len_v == 0 then line = "\n" else local offset = start + len_v + 1 local ch = string.sub(label, offset, offset) if ch == "\n" then line = lines[i] .. "\n" end end if line then lines[i] = line end start = start + #lines[i] end if string.sub(label, #label, #label) == '\n' then insert(lines, "") end if #lines == 0 then insert(lines, "") end return lines end function Text.CreateObject() return love.graphics.newText(Style.Font) end return Text ================================================ FILE: Internal/UI/Tooltip.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local format = string.format local min = math.min local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local Tooltip = {} local LastDisplayTime = 0.0 local AccumDisplayTime = 0.0 local TooltipTime = 0.75 local TooltipExpireTime = 0.025 local Alpha = 0.0 local OffsetY = 0.0 local ResetSize = false function Tooltip.Begin(Tip) if Tip == nil or Tip == "" then return end local Elapsed = love.timer.getTime() - LastDisplayTime if Elapsed > TooltipExpireTime then AccumDisplayTime = 0.0 Alpha = 0.0 ResetSize = true end local DeltaTime = love.timer.getDelta() AccumDisplayTime = AccumDisplayTime + DeltaTime LastDisplayTime = love.timer.getTime() if AccumDisplayTime > TooltipTime then local X, Y = Mouse.Position() Alpha = min(Alpha + DeltaTime * 4.0, 1.0) local BgColor = Utility.MakeColor(Style.WindowBackgroundColor) local TextColor = Utility.MakeColor(Style.TextColor) BgColor[4] = Alpha TextColor[4] = Alpha local CursorX, CursorY = Cursor.GetPosition() LayoutManager.Begin('Ignore', {Ignore = true}) Window.Begin('tooltip', { X = X, Y = Y - OffsetY, W = 0, H = 0, AutoSizeWindow = true, AutoSizeContent = false, AllowResize = false, AllowFocus = false, Layer = 'ContextMenu', ResetWindowSize = ResetSize, CanObstruct = false, NoSavedSettings = true }) Text.BeginFormatted(Tip, {Color = TextColor}) OffsetY = Window.GetHeight() Window.End() LayoutManager.End() Cursor.SetPosition(CursorX, CursorY) ResetSize = false end end function Tooltip.GetDebugInfo() local Info = {} local Elapsed = love.timer.getTime() - LastDisplayTime insert(Info, format("Time: %.2f", AccumDisplayTime)) insert(Info, format("Is Visible: %s", tostring(AccumDisplayTime > TooltipTime and Elapsed <= TooltipExpireTime))) insert(Info, format("Time to Display: %.2f", TooltipTime)) insert(Info, format("Expire Time: %f", TooltipExpireTime)) return Info end return Tooltip ================================================ FILE: Internal/UI/Tree.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local max = math.max local insert = table.insert local remove = table.remove local Button = require(SLAB_PATH .. '.Internal.UI.Button') local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local Image = require(SLAB_PATH .. '.Internal.UI.Image') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Messages = require(SLAB_PATH .. '.Internal.Core.Messages') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Text = require(SLAB_PATH .. '.Internal.UI.Text') local Tooltip = require(SLAB_PATH .. '.Internal.UI.Tooltip') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local IdCache = require(SLAB_PATH .. '.Internal.Core.IdCache') local Tree = {} local Instances = setmetatable({}, {__mode = "k"}) local Hierarchy = {} local Radius = 4.0 local idCache = IdCache() local EMPTY = {} local IGNORE = { Ignore = true } local CENTERY = { CenterY = true } local TRANSPARENT = { 0, 0, 0, 0 } local function GetInstance(Id) local IdString = tostring(Id) if #Hierarchy > 0 then IdString = idCache:get(Hierarchy[1].Id, IdString) end if Instances[Id] == nil then local Instance = {} Instance.X = 0.0 Instance.Y = 0.0 Instance.W = 0.0 Instance.H = 0.0 Instance.IsOpen = false Instance.WasOpen = false Instance.Id = IdString Instance.StatHandle = nil Instance.TreeR = 0 Instance.TreeB = 0 Instance.NoSavedSettings = false Instances[Id] = Instance end return Instances[Id] end function Tree.Begin(Id, Options) local StatHandle = Stats.Begin('Tree', 'Slab') local IsTableId = type(Id) == 'table' local IdLabel = tostring(Id) Options = Options or EMPTY local label = Options.Label or IdLabel local icon = Options.Icon local openWithHighlight = Options.OpenWithHighlight == nil or Options.OpenWithHighlight if Options.IconPath ~= nil then Messages.Broadcast('Tree.Options.IconPath', "'IconPath' option has been deprecated since v0.8.0. Please use the 'Icon' option.") end local Instance = nil local WinItemId = Window.GetItemId(IdLabel) if IsTableId then Instance = GetInstance(Id) else Instance = GetInstance(WinItemId) end Instance.WasOpen = Instance.IsOpen Instance.StatHandle = StatHandle Instance.NoSavedSettings = Options.NoSavedSettings or IsTableId local MouseX, MouseY = Mouse.Position() local TMouseX, TMouseY = Region.InverseTransform(nil, MouseX, MouseY) local WinX, WinY = Window.GetPosition() local WinW, WinH = Window.GetBorderlessSize() local IsObstructed = Window.IsObstructedAtMouse() or Region.IsHoverScrollBar() local W = Text.GetWidth(label) local H = max(Style.Font:getHeight(), Instance.H) if not Options.IsLeaf then W = W + H + Radius end -- Account for icon if one is requested. W = icon and W + H or W WinX = WinX + Window.GetBorder() WinY = WinY + Window.GetBorder() if #Hierarchy == 0 then local ControlW, ControlH = W, H if Instance.TreeR > 0 and Instance.TreeB > 0 then ControlW = Instance.TreeR - Instance.X ControlH = Instance.TreeB - Instance.Y end LayoutManager.AddControl(ControlW, ControlH, 'Tree') Instance.TreeR = 0 Instance.TreeB = 0 end local Root = Instance if #Hierarchy > 0 then Root = Hierarchy[#Hierarchy] end local X, Y = Cursor.GetPosition() if Root ~= Instance then X = Root ~= Instance and (Root.X + H * #Hierarchy) Cursor.SetX(X) end local TriX, TriY = X + Radius, Y + H * 0.5 local IsHot = not IsObstructed and WinX <= TMouseX and TMouseX <= WinX + WinW and Y <= TMouseY and TMouseY <= Y + H and Region.Contains(MouseX, MouseY) if IsHot or Options.IsSelected then DrawCommands.Rectangle('fill', WinX, Y, WinW, H, Style.TextHoverBgColor) end if IsHot then if Mouse.IsClicked(1) and not Options.IsLeaf and openWithHighlight then Instance.IsOpen = not Instance.IsOpen end end local IsExpanderClicked = false local ExpandIconOptions = Instance.ExpandIconOptions if not Options.IsLeaf then -- Render the triangle depending on if the tree item is open/closed. local SubX = Instance.IsOpen and 0 or 200 local SubY = Instance.IsOpen and 100 or 50 if not ExpandIconOptions then ExpandIconOptions = { Image = { Path = SLAB_FILE_PATH .. '/Internal/Resources/Textures/Icons.png', SubW = 50, SubH = 50 }, PadX = 0, PadY = 0, Color = TRANSPARENT, HoverColor = TRANSPARENT, PressColor = TRANSPARENT, IconId = Instance.Id .. '_Open', ExpandId = Instance.Id .. '_Expand', } Instance.ExpandIconOptions = ExpandIconOptions end ExpandIconOptions.Image.SubX = SubX ExpandIconOptions.Image.SubY = SubY ExpandIconOptions.W = H ExpandIconOptions.H = H if Button.Begin(ExpandIconOptions.ExpandId, ExpandIconOptions) and not openWithHighlight then Instance.IsOpen = not Instance.IsOpen Window.SetHotItem(nil) IsExpanderClicked = true end Cursor.SameLine() else -- Advance the cursor for leaf nodes so text aligns with other items that are not leaf nodes. Cursor.AdvanceX(H + Cursor.PadX()) end if not Instance.IsOpen and Instance.WasOpen then Window.ResetContentSize() Region.ResetContentSize() end Instance.X = X Instance.Y = Y Instance.W = W Instance.H = H LayoutManager.Begin('Ignore', IGNORE) if icon ~= nil then -- Force the icon to be of the same size as the tree item. icon.W = H icon.H = H Image.Begin(ExpandIconOptions.IconId, icon) local ItemX, ItemY, ItemW, ItemH = Cursor.GetItemBounds() Instance.H = max(Instance.H, ItemH) Cursor.SameLine(CENTERY) end Text.Begin(label) LayoutManager.End() local ItemX, ItemY, ItemW, ItemH = Cursor.GetItemBounds() Root.TreeR = max(Root.TreeR, ItemX + ItemW) Root.TreeB = max(Root.TreeB, Y + H) Cursor.SetY(Instance.Y) Cursor.AdvanceY(H) if Instance.IsOpen then insert(Hierarchy, 1, Instance) end if IsHot then Tooltip.Begin(Options.Tooltip or "") if not IsExpanderClicked then Window.SetHotItem(WinItemId) end end -- The size of the item has already been determined by Text.Begin. However, this item's ID needs to be -- set as the last item for hot item checks. So the item will be added with zero width and height. Window.AddItem(X, Y, 0, 0, WinItemId) if not Instance.IsOpen then Stats.End(Instance.StatHandle) end return Instance.IsOpen end function Tree.End() local StatHandle = Hierarchy[1].StatHandle remove(Hierarchy, 1) Stats.End(StatHandle) end function Tree.Save(Table) if Table ~= nil then local Settings = {} for K, V in ipairs(Instances) do if not V.NoSavedSettings then Settings[V.Id] = { IsOpen = V.IsOpen } end end Table['Tree'] = Settings end end function Tree.Load(Table) if Table ~= nil then local Settings = Table['Tree'] if Settings ~= nil then for K, V in pairs(Settings) do local Instance = GetInstance(K) Instance.IsOpen = V.IsOpen end end end end function Tree.GetDebugInfo() local Result = {} for K, V in pairs(Instances) do table.insert(Result, tostring(K)) end return Result end return Tree ================================================ FILE: Internal/UI/Window.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local insert = table.insert local remove = table.remove local max = math.max local floor = math.floor local Cursor = require(SLAB_PATH .. ".Internal.Core.Cursor") local Dock = require(SLAB_PATH .. '.Internal.UI.Dock') local DrawCommands = require(SLAB_PATH .. ".Internal.Core.DrawCommands") local MenuState = require(SLAB_PATH .. ".Internal.UI.MenuState") local Mouse = require(SLAB_PATH .. ".Internal.Input.Mouse") local Region = require(SLAB_PATH .. ".Internal.UI.Region") local Stats = require(SLAB_PATH .. ".Internal.Core.Stats") local Style = require(SLAB_PATH .. ".Style") local Utility = require(SLAB_PATH .. ".Internal.Core.Utility") local IdCache = require(SLAB_PATH .. ".Internal.Core.IdCache") local Scale = require(SLAB_PATH .. ".Internal.Core.Scale") local Window = {} local Instances = {} local Stack = {} local StackLockId = nil local PendingStack = {} local ActiveInstance = nil local MovingInstance = nil local IDStack = {} local idCache = IdCache() local MenuBarInstance local SizerNone = 0 local SizerN = 1 local SizerE = 2 local SizerS = 3 local SizerW = 4 local SizerNE = 5 local SizerSE = 6 local SizerSW = 7 local SizerNW = 8 local SizerCursor = { [SizerNW] = 'sizenwse', [SizerNE] = 'sizenesw', [SizerSE] = 'sizenwse', [SizerSW] = 'sizenesw', [SizerW] = 'sizewe', [SizerE] = 'sizewe', [SizerN] = 'sizens', [SizerS] = 'sizens', } local EMPTY = {} local TitleRounding = { 0, 0, 0, 0 } local BodyRounding = { 0, 0, 0, 0 } local function UpdateStackIndex() for i = 1, #Stack do Stack[i].StackIndex = #Stack - i + 1 end end local function PushToTop(instance) for i, v in ipairs(Stack) do if instance == v then remove(Stack, i) break end end insert(Stack, 1, instance) UpdateStackIndex() end local function NewInstance(id) local instance = { Id = id, X = 0, Y = 0, W = 200, H = 200, ContentW = 0, ContentH = 0, Title = "", IsMoving = false, TitleDeltaX = 0, TitleDeltaY = 0, AllowResize = true, AllowFocus = true, SizerType = SizerNone, SizerFilter = nil, SizeDeltaX = 0, SizeDeltaY = 0, HasResized = false, DeltaContentW = 0, DeltaContentH = 0, BackgroundColor = Style.WindowBackgroundColor, Border = 4, Children = {}, LastItem = nil, HotItem = nil, ContextHotItem = nil, Items = {}, Layer = 'Normal', StackIndex = 0, CanObstruct = true, FrameNumber = 0, LastCursorX = 0, LastCursorY = 0, StatHandle = nil, IsAppearing = false, IsOpen = true, IsContentOpen = true, IsMinimized = false, NoSavedSettings = false, TitleId = id .. '_Title', TitleRegion = { NoBackground = true, NoOutline = true, IgnoreScroll = true, }, InstanceRegion = {}, ShowScrollbarX = false, ShowScrollbarY = false, } return instance end local function GetInstance(id) if id == nil then return ActiveInstance end for i, v in ipairs(Instances) do if v.Id == id then return v end end local instance = NewInstance(id) insert(Instances, instance) return instance end local function Contains(instance, x, y) if instance ~= nil then local titleH = instance.TitleH or 0 return instance.X <= x and x <= instance.X + instance.W and instance.Y - titleH <= y and y <= instance.Y + instance.H end return false end local function UpdateTitleBar(instance, isObstructed, allowMove, constrain) if isObstructed and (instance.IsContentOpen == nil or instance.IsContentOpen) then return end if instance ~= nil and instance.Title ~= "" and instance.SizerType == SizerNone then local w = instance.W local h = instance.TitleH local x = instance.X local y = instance.Y - h local isTethered = Dock.IsTethered(instance.Id) local mouseX, mouseY = Mouse.Position() if Mouse.IsClicked(1) then if x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then if allowMove then instance.IsMoving = true end if isTethered then Dock.BeginTear(instance.Id, mouseX, mouseY) end if instance.AllowFocus then PushToTop(instance) end end elseif Mouse.IsReleased(1) then instance.IsMoving = false -- Prevent window going behind MenuBar if MenuBarInstance and Contains(MenuBarInstance, mouseX, mouseY) then instance.TitleDeltaY = MenuBarInstance.H end end if instance.IsMoving then local deltaX, deltaY = Mouse.GetDelta() local titleDeltaX, titleDeltaY = instance.TitleDeltaX, instance.TitleDeltaY instance.TitleDeltaX = instance.TitleDeltaX + deltaX instance.TitleDeltaY = instance.TitleDeltaY + deltaY if constrain then -- Constrain the position of the window to the viewport. The position at this point in the code has the delta already applied. This delta will need to be -- removed to retrieve the original position, and clamp the delta based off of that posiiton. local originX = instance.X - titleDeltaX local originY = instance.Y - titleDeltaY - instance.TitleH instance.TitleDeltaX = Utility.Clamp(instance.TitleDeltaX, -originX, Scale.GetScreenWidth() - (originX + instance.W)) instance.TitleDeltaY = Utility.Clamp(instance.TitleDeltaY, -originY + MenuState.MainMenuBarH, Scale.GetScreenHeight() - (originY + instance.H + instance.TitleH)) end elseif isTethered then Dock.UpdateTear(instance.Id, mouseX, mouseY) -- Retrieve the cached options to calculate torn off position. The cached options contain the -- desired bounds for this window. The bounds that are a part of the Instance are the altered options -- modified by the Dock module. local options = Dock.GetCachedOptions(instance.Id) if not Dock.IsTethered(instance.Id) then instance.IsMoving = true if options ~= nil then -- Properly place the window at the mouse position offset by the title width/height. instance.TitleDeltaX = mouseX - x - floor(w * 0.25) instance.TitleDeltaY = mouseY - y - floor(h * 0.5) end end end end end local function IsSizerEnabled(instance, sizer) if not instance then return false end if #instance.SizerFilter == 0 then return true end for i, v in ipairs(instance.SizerFilter) do if v == sizer then return true end end return false end local function UpdateSize(instance, isObstructed) if not instance or not instance.AllowResize then return end if Region.IsHoverScrollBar(instance.Id) then return end if instance.SizerType == SizerNone and isObstructed then return end if MovingInstance ~= nil then return end local x, y, w, h = instance.X, instance.Y, instance.W, instance.H if instance.Title ~= "" then y = y - instance.TitleH h = h + instance.TitleH end local mouseX, mouseY = Mouse.Position() local newSizerType = SizerNone local scrollPad = Region.GetScrollPad() if x <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + h then if x <= mouseX and mouseX <= x + scrollPad and y <= mouseY and mouseY <= y + scrollPad and IsSizerEnabled(instance, "NW") then newSizerType = SizerNW elseif x + w - scrollPad <= mouseX and mouseX <= x + w and y <= mouseY and mouseY <= y + scrollPad and IsSizerEnabled(instance, "NE") then newSizerType = SizerNE elseif x + w - scrollPad <= mouseX and mouseX <= x + w and y + h - scrollPad <= mouseY and mouseY <= y + h and IsSizerEnabled(instance, "SE") then newSizerType = SizerSE elseif x <= mouseX and mouseX <= x + scrollPad and y + h - scrollPad <= mouseY and mouseY <= y + h and IsSizerEnabled(instance, "SW") then newSizerType = SizerSW elseif x <= mouseX and mouseX <= x + scrollPad and IsSizerEnabled(instance, "W") then newSizerType = SizerW elseif x + w - scrollPad <= mouseX and mouseX <= x + w and IsSizerEnabled(instance, "E") then newSizerType = SizerE elseif y <= mouseY and mouseY <= y + scrollPad and IsSizerEnabled(instance, "N") then newSizerType = SizerN elseif y + h - scrollPad <= mouseY and mouseY <= y + h and IsSizerEnabled(instance, "S") then newSizerType = SizerS end if SizerCursor[newSizerType] then Mouse.SetCursor(SizerCursor[newSizerType]) end end if Mouse.IsClicked(1) then instance.SizerType = newSizerType elseif Mouse.IsReleased(1) then instance.SizerType = SizerNone end if instance.SizerType ~= SizerNone then local deltaX, deltaY = Mouse.GetDelta() if instance.W <= instance.Border then if (instance.SizerType == SizerW or instance.SizerType == SizerNW or instance.SizerType == SizerSW) and deltaX > 0 then deltaX = 0 end if (instance.SizerType == SizerE or instance.SizerType == SizerNE or instance.SizerType == SizerSE) and deltaX < 0 then deltaX = 0 end end if instance.H <= instance.Border then if (instance.SizerType == SizerN or instance.SizerType == SizerNW or instance.SizerType == SizerNE) and deltaY > 0 then deltaY = 0 end if (instance.SizerType == SizerS or instance.SizerType == SizerSE or instance.SizerType == SizerSW) and deltaY < 0 then deltaY = 0 end end if deltaX ~= 0 or deltaY ~= 0 then instance.HasResized = true instance.DeltaContentW = 0 instance.DeltaContentH = 0 end if instance.SizerType == SizerN then instance.TitleDeltaY = instance.TitleDeltaY + deltaY instance.SizeDeltaY = instance.SizeDeltaY - deltaY elseif instance.SizerType == SizerE then instance.SizeDeltaX = instance.SizeDeltaX + deltaX elseif instance.SizerType == SizerS then instance.SizeDeltaY = instance.SizeDeltaY + deltaY elseif instance.SizerType == SizerW then instance.TitleDeltaX = instance.TitleDeltaX + deltaX instance.SizeDeltaX = instance.SizeDeltaX - deltaX elseif instance.SizerType == SizerNW then instance.TitleDeltaX = instance.TitleDeltaX + deltaX instance.SizeDeltaX = instance.SizeDeltaX - deltaX instance.TitleDeltaY = instance.TitleDeltaY + deltaY instance.SizeDeltaY = instance.SizeDeltaY - deltaY elseif instance.SizerType == SizerNE then instance.SizeDeltaX = instance.SizeDeltaX + deltaX instance.TitleDeltaY = instance.TitleDeltaY + deltaY instance.SizeDeltaY = instance.SizeDeltaY - deltaY elseif instance.SizerType == SizerSE then instance.SizeDeltaX = instance.SizeDeltaX + deltaX instance.SizeDeltaY = instance.SizeDeltaY + deltaY elseif instance.SizerType == SizerSW then instance.TitleDeltaX = instance.TitleDeltaX + deltaX instance.SizeDeltaX = instance.SizeDeltaX - deltaX instance.SizeDeltaY = instance.SizeDeltaY + deltaY end if SizerCursor[instance.SizerType] then Mouse.SetCursor(SizerCursor[instance.SizerType]) end end end local function DrawButton(type, activeInstance, options, radius, offsetX, offsetY, hoverColor, color) local isClicked = false local mouseX, mouseY = Mouse.Position() local isObstructed = false if type == "Close" then isObstructed = Window.IsObstructed(mouseX, mouseY, true) end local size = radius * 0.5 local x = activeInstance.X + activeInstance.W - radius * offsetX local y = activeInstance.Y - offsetY * 0.5 local isHovered = x - radius <= mouseX and mouseX <= x + radius and y - offsetY * 0.5 <= mouseY and mouseY <= y + radius and not isObstructed if isHovered then DrawCommands.Circle('fill', x, y, radius, hoverColor) if Mouse.IsClicked(1) then isClicked = true end end if type == "Close" then DrawCommands.Cross(x, y, size, color) elseif type == "Minimize" then if activeInstance.IsMinimized then DrawCommands.Rectangle("line", x - size, y - size, size * 2, size * 2, color) else DrawCommands.Line(x - size, y, x + size, y, 2, color) end end return isClicked end function Window.Top() return ActiveInstance end function Window.IsObstructed(x, y, skipScrollCheck) if Region.IsScrolling() then return true end -- If there are no windows, then nothing can obstruct. if #Stack == 0 then return false end if ActiveInstance ~= nil then if not ActiveInstance.IsOpen then return true end if ActiveInstance.IsContentOpen == false then return true end if ActiveInstance.IsMoving then return false end if ActiveInstance.IsAppearing then return true end -- Gather all potential windows that can obstruct the given position. local list = {} for i, v in ipairs(Stack) do -- Stack locks prevents other windows to be considered. if v.Id == StackLockId then insert(list, v) break end if Contains(v, x, y) and v.CanObstruct then insert(list, v) end end -- Certain layers are rendered on top of 'Normal' windows. Consider these windows first. local top = nil for i, v in ipairs(list) do if v.Layer ~= 'Normal' then top = v break end end -- If all windows are considered the normal layer, then just grab the window at the top of the stack. if top == nil then top = list[1] end if top ~= nil then if ActiveInstance == top then if not skipScrollCheck and Region.IsHoverScrollBar(ActiveInstance.Id) then return true end return false elseif top.IsOpen then return true end end end return false end function Window.IsObstructedAtMouse() local x, y = Mouse.Position() return Window.IsObstructed(x, y) end function Window.Reset() for i = 1, #PendingStack do PendingStack[i] = nil end ActiveInstance = GetInstance('Global') ActiveInstance.W, ActiveInstance.H = Scale.GetScreenDimensions() ActiveInstance.Border = 0 ActiveInstance.NoSavedSettings = true insert(PendingStack, 1, ActiveInstance) MenuBarInstance = nil end function Window.Begin(id, options) local statHandle = Stats.Begin('Window', 'Slab') options = options or EMPTY if not Mouse.IsDragging(1) then Dock.AlterOptions(id, options) end local x = options.X or 50 local y = options.Y or 50 local w = options.W or 200 local h = options.H or 200 local contentW = options.ContentW or 0 local contentH = options.ContentH or 0 local bgColor = options.BgColor or Style.WindowBackgroundColor local title = options.Title or "" local titleAlignX = options.TitleAlignX or 'center' local titleAlignY = options.TitleAlignY or 'center' local titleH = options.TitleH == nil and ((title ~= nil and title ~= "") and max(Style.WindowTitleH, Style.Font:getHeight()) or 0) or options.TitleH local allowMove = options.AllowMove == nil or options.AllowMove local allowResize = options.AllowResize == nil or options.AllowResize local allowFocus = options.AllowFocus == nil or options.AllowFocus local border = options.Border or Style.WindowBorder local autoSizeWindow = options.AutoSizeWindow == nil or options.AutoSizeWindow local autoSizeWindowW = options.AutoSizeWindowW or autoSizeWindow local autoSizeWindowH = options.AutoSizeWindowH or autoSizeWindow local autoSizeContent = options.AutoSizeContent == nil or options.AutoSizeContent local layer = options.Layer or 'Normal' local resetSize = options.ResetSize or autoSizeWindow local resetContent = options.ResetContent or autoSizeContent local sizerFilter = options.SizerFilter or EMPTY local canObstruct = options.CanObstruct == nil or options.CanObstruct local rounding = options.Rounding or Style.WindowRounding local showMinimize = options.ShowMinimize == nil or options.ShowMinimize local showScrollbarX = not not options.ShowScrollbarX local showScrollbarY = not not options.ShowScrollbarY TitleRounding[1], TitleRounding[2] = rounding, rounding BodyRounding[3], BodyRounding[4] = rounding, rounding local titleRounding, bodyRounding = TitleRounding, BodyRounding if type(rounding) == 'table' then titleRounding = rounding bodyRounding = rounding elseif title == "" then bodyRounding = rounding end local instance = GetInstance(id) insert(PendingStack, 1, instance) if options.IsMenuBar then MenuBarInstance = instance end if ActiveInstance ~= nil then ActiveInstance.Children[id] = instance end ActiveInstance = instance if autoSizeWindowW then w = 0 end if autoSizeWindowH then h = 0 end if options.ResetPosition or options.ResetLayout then ActiveInstance.TitleDeltaX = 0 ActiveInstance.TitleDeltaY = 0 end if ActiveInstance.AutoSizeWindow ~= autoSizeWindow and autoSizeWindow then resetSize = true end if ActiveInstance.Border ~= border then resetSize = true end ActiveInstance.X = ActiveInstance.TitleDeltaX + x ActiveInstance.Y = ActiveInstance.TitleDeltaY + y ActiveInstance.W = max(ActiveInstance.SizeDeltaX + w + border, border) ActiveInstance.H = max(ActiveInstance.SizeDeltaY + h + border, border) ActiveInstance.ContentW = contentW ActiveInstance.ContentH = contentH ActiveInstance.BackgroundColor = bgColor ActiveInstance.Title = title ActiveInstance.TitleH = titleH ActiveInstance.AllowResize = allowResize and not autoSizeWindow ActiveInstance.AllowFocus = allowFocus ActiveInstance.Border = border ActiveInstance.IsMenuBar = options.IsMenuBar ActiveInstance.AutoSizeWindow = autoSizeWindow ActiveInstance.AutoSizeWindowW = autoSizeWindowW ActiveInstance.AutoSizeWindowH = autoSizeWindowH ActiveInstance.AutoSizeContent = autoSizeContent ActiveInstance.Layer = layer ActiveInstance.HotItem = nil ActiveInstance.SizerFilter = sizerFilter ActiveInstance.HasResized = false ActiveInstance.CanObstruct = canObstruct ActiveInstance.StatHandle = statHandle ActiveInstance.NoSavedSettings = options.NoSavedSettings ActiveInstance.ShowMinimize = showMinimize ActiveInstance.ShowScrollbarX = showScrollbarX ActiveInstance.ShowScrollbarY = showScrollbarY local showClose = false if options.IsOpen ~= nil and type(options.IsOpen) == 'boolean' then ActiveInstance.IsOpen = options.IsOpen showClose = true end if options.IsContentOpen ~= nil and type(options.IsContentOpen) == "boolean" then ActiveInstance.IsContentOpen = options.IsContentOpen end if ActiveInstance.IsOpen then local currentFrameNumber = Stats.GetFrameNumber() ActiveInstance.IsAppearing = currentFrameNumber - ActiveInstance.FrameNumber > 1 ActiveInstance.FrameNumber = currentFrameNumber if ActiveInstance.StackIndex == 0 then insert(Stack, 1, ActiveInstance) UpdateStackIndex() end end if ActiveInstance.AutoSizeContent then ActiveInstance.ContentW = max(contentW, ActiveInstance.DeltaContentW) ActiveInstance.ContentH = max(contentH, ActiveInstance.DeltaContentH) end local offsetY = ActiveInstance.TitleH if ActiveInstance.Title ~= "" then ActiveInstance.Y = ActiveInstance.Y + offsetY if autoSizeWindow then local titleW = Style.Font:getWidth(ActiveInstance.Title) + ActiveInstance.Border * 2 ActiveInstance.W = max(ActiveInstance.W, titleW) end end local mouseX, mouseY = Mouse.Position() local isObstructed = Window.IsObstructed(mouseX, mouseY, true) if (ActiveInstance.AllowFocus and Mouse.IsClicked(1) and not isObstructed and Contains(ActiveInstance, mouseX, mouseY)) or ActiveInstance.IsAppearing then PushToTop(ActiveInstance) end instance.LastCursorX, instance.LastCursorY = Cursor.GetPosition() Cursor.SetPosition(ActiveInstance.X + ActiveInstance.Border, ActiveInstance.Y + ActiveInstance.Border) Cursor.SetAnchor(ActiveInstance.X + ActiveInstance.Border, ActiveInstance.Y + ActiveInstance.Border) UpdateSize(ActiveInstance, isObstructed) UpdateTitleBar(ActiveInstance, isObstructed, allowMove, options.ConstrainPosition) DrawCommands.SetLayer(ActiveInstance.Layer) DrawCommands.Begin(ActiveInstance.StackIndex) if ActiveInstance.Title ~= "" then local closeBgRadius = offsetY * 0.4 local minimizeBgRadius = offsetY * 0.4 local titleX = floor(ActiveInstance.X + (ActiveInstance.W * 0.5) - (Style.Font:getWidth(ActiveInstance.Title) * 0.5)) local titleY = floor(ActiveInstance.Y - offsetY * 0.5 - Style.Font:getHeight() * 0.5) -- Check for horizontal alignment. if titleAlignX == 'left' then titleX = floor(ActiveInstance.X + ActiveInstance.Border) elseif titleAlignX == 'right' then titleX = floor(ActiveInstance.X + ActiveInstance.W - Style.Font:getWidth(ActiveInstance.Title) - ActiveInstance.Border) if showClose then titleX = floor(titleX - closeBgRadius * 2) end if showMinimize then titleX = floor(titleX - minimizeBgRadius * 2) end end -- Check for vertical alignment if titleAlignY == 'top' then titleY = floor(ActiveInstance.Y - offsetY) elseif titleAlignY == 'bottom' then titleY = floor(ActiveInstance.Y - Style.Font:getHeight()) end local titleColor = ActiveInstance.BackgroundColor if ActiveInstance == Stack[1] then titleColor = Style.WindowTitleFocusedColor end DrawCommands.Rectangle('fill', ActiveInstance.X, ActiveInstance.Y - offsetY, ActiveInstance.W, offsetY, titleColor, titleRounding) DrawCommands.Rectangle('line', ActiveInstance.X, ActiveInstance.Y - offsetY, ActiveInstance.W, offsetY, nil, titleRounding) DrawCommands.Line(ActiveInstance.X, ActiveInstance.Y, ActiveInstance.X + ActiveInstance.W, ActiveInstance.Y, 1) do local titleRegion = ActiveInstance.TitleRegion titleRegion.X = ActiveInstance.X titleRegion.MouseX = mouseX titleRegion.MouseY = mouseY titleRegion.IsObstructed = isObstructed titleRegion.Y = ActiveInstance.Y - offsetY titleRegion.W = ActiveInstance.W titleRegion.H = offsetY Region.Begin(ActiveInstance.TitleId, titleRegion) end DrawCommands.Print(ActiveInstance.Title, titleX, titleY, Style.TextColor, Style.Font) local offsetX = 1 if showMinimize then offsetX = showClose and 5 or 2 local isClicked = DrawButton( "Minimize", ActiveInstance, options, minimizeBgRadius, offsetX, offsetY, Style.WindowMinimizeColorBgColor or Style.WindowCloseBgColor, Style.WindowMinimizeColor or Style.WindowCloseColor ) if isClicked then ActiveInstance.IsContentOpen = not ActiveInstance.IsContentOpen ActiveInstance.IsMoving = false ActiveInstance.IsMinimized = not ActiveInstance.IsMinimized end end if showClose then offsetX = 2 local isClicked = DrawButton( "Close", ActiveInstance, options, closeBgRadius, offsetX, offsetY, Style.WindowCloseBgColor, Style.WindowCloseColor ) if isClicked then ActiveInstance.IsOpen = false ActiveInstance.IsMoving = false options.IsOpen = false end end Region.End() end local regionW = ActiveInstance.W local regionH = ActiveInstance.H if ActiveInstance.X + ActiveInstance.W > Scale.GetScreenWidth() then regionW = Scale.GetScreenWidth() - ActiveInstance.X end if ActiveInstance.Y + ActiveInstance.H > Scale.GetScreenHeight() then regionH = Scale.GetScreenHeight() - ActiveInstance.Y end if ActiveInstance.IsContentOpen == false then regionW = 0 regionH = 0 ActiveInstance.ContentW = 0 ActiveInstance.ContentH = 0 end do local region = ActiveInstance.InstanceRegion region.X = ActiveInstance.X region.Y = ActiveInstance.Y region.W = regionW region.H = regionH region.ContentW = ActiveInstance.ContentW + ActiveInstance.Border region.ContentH = ActiveInstance.ContentH + ActiveInstance.Border region.BgColor = ActiveInstance.BackgroundColor region.IsObstructed = isObstructed region.MouseX = mouseX region.MouseY = mouseY region.ResetContent = ActiveInstance.HasResized region.Rounding = bodyRounding region.NoOutline = options.NoOutline if ActiveInstance.ShowScrollbarX or ActiveInstance.ShowScrollbarY then region.IsObstructed = false end region.ForceShowX = showScrollbarX region.ForceShowY = showScrollbarY Region.Begin(ActiveInstance.Id, region) end if resetSize or options.ResetLayout then ActiveInstance.SizeDeltaX = 0 ActiveInstance.SizeDeltaY = 0 end if resetContent or options.ResetLayout then ActiveInstance.DeltaContentW = 0 ActiveInstance.DeltaContentH = 0 end return ActiveInstance.IsOpen end function Window.End() if not ActiveInstance then return end local handle = ActiveInstance.StatHandle -- Clear the ID stack for use with other windows. This information can be kept transient for i = 1, #IDStack do IDStack[i] = nil end Region.End() DrawCommands.End(not ActiveInstance.IsOpen) remove(PendingStack, 1) Cursor.SetPosition(ActiveInstance.LastCursorX, ActiveInstance.LastCursorY) ActiveInstance = nil if #PendingStack > 0 then ActiveInstance = PendingStack[1] Cursor.SetAnchor(ActiveInstance.X + ActiveInstance.Border, ActiveInstance.Y + ActiveInstance.Border) DrawCommands.SetLayer(ActiveInstance.Layer) Region.ApplyScissor() end Stats.End(handle) end function Window.GetMousePosition() local x, y = Mouse.Position() if ActiveInstance ~= nil then x, y = Region.InverseTransform(nil, x, y) end return x, y end function Window.GetWidth() if ActiveInstance ~= nil then return ActiveInstance.W end return 0 end function Window.GetHeight() if ActiveInstance ~= nil then return ActiveInstance.H end return 0 end function Window.GetBorder() if ActiveInstance ~= nil then return ActiveInstance.Border end return 0 end function Window.GetBounds(IgnoreTitleBar) if ActiveInstance ~= nil then IgnoreTitleBar = IgnoreTitleBar == nil and false or IgnoreTitleBar local offsetY = (ActiveInstance.Title ~= "" and not IgnoreTitleBar) and ActiveInstance.TitleH or 0 return ActiveInstance.X, ActiveInstance.Y - offsetY, ActiveInstance.W, ActiveInstance.H + offsetY end return 0, 0, 0, 0 end function Window.GetPosition() if ActiveInstance ~= nil then return ActiveInstance.X, ActiveInstance.Y - ActiveInstance.TitleH end return 0, 0 end function Window.GetSize() if ActiveInstance ~= nil then return ActiveInstance.W, ActiveInstance.H end return 0, 0 end function Window.GetContentSize() if ActiveInstance ~= nil then return ActiveInstance.ContentW, ActiveInstance.ContentH end return 0, 0 end --[[ This function is used to help other controls retrieve the available real estate needed to expand their bounds without expanding the bounds of the window by removing borders. --]] function Window.GetBorderlessSize() local w, h = 0, 0 if ActiveInstance ~= nil then w = max(ActiveInstance.W, ActiveInstance.ContentW) h = max(ActiveInstance.H, ActiveInstance.ContentH) w = max(0, w - ActiveInstance.Border * 2) h = max(0, h - ActiveInstance.Border * 2) end return w, h end function Window.GetRemainingSize() local w, h = Window.GetBorderlessSize() if ActiveInstance ~= nil then w = w - (Cursor.GetX() - ActiveInstance.X - ActiveInstance.Border) h = h - (Cursor.GetY() - ActiveInstance.Y - ActiveInstance.Border) end return w, h end function Window.IsMenuBar() if ActiveInstance ~= nil then return ActiveInstance.IsMenuBar end return false end function Window.GetId() if ActiveInstance ~= nil then return ActiveInstance.Id end return '' end function Window.AddItem(x, y, w, h, id) if ActiveInstance ~= nil then ActiveInstance.LastItem = id if Region.IsActive(ActiveInstance.Id) then if ActiveInstance.AutoSizeWindowW then ActiveInstance.SizeDeltaX = max(ActiveInstance.SizeDeltaX, x + w - ActiveInstance.X) end if ActiveInstance.AutoSizeWindowH then ActiveInstance.SizeDeltaY = max(ActiveInstance.SizeDeltaY, y + h - ActiveInstance.Y) end if ActiveInstance.AutoSizeContent then ActiveInstance.DeltaContentW = max(ActiveInstance.DeltaContentW, x + w - ActiveInstance.X) ActiveInstance.DeltaContentH = max(ActiveInstance.DeltaContentH, y + h - ActiveInstance.Y) end else Region.AddItem(x, y, w, h) end end end function Window.WheelMoved(x, y) Region.WheelMoved(x, y) end function Window.TransformPoint(x, y) if ActiveInstance ~= nil then return Region.Transform(ActiveInstance.Id, x, y) end return 0, 0 end function Window.ResetContentSize() if ActiveInstance ~= nil then ActiveInstance.DeltaContentW = 0 ActiveInstance.DeltaContentH = 0 end end function Window.SetHotItem(HotItem) if ActiveInstance ~= nil then ActiveInstance.HotItem = HotItem end end function Window.SetContextHotItem(HotItem) if ActiveInstance ~= nil then ActiveInstance.ContextHotItem = HotItem end end function Window.GetHotItem() if ActiveInstance ~= nil then return ActiveInstance.HotItem end return nil end function Window.IsItemHot() if ActiveInstance ~= nil and ActiveInstance.LastItem ~= nil then return ActiveInstance.HotItem == ActiveInstance.LastItem end return false end function Window.GetContextHotItem() if ActiveInstance ~= nil then return ActiveInstance.ContextHotItem end return nil end function Window.IsMouseHovered() if ActiveInstance ~= nil then local x, y = Mouse.Position() return Contains(ActiveInstance, x, y) end return false end function Window.GetItemId(id) if ActiveInstance ~= nil then if ActiveInstance.Items[id] == nil then ActiveInstance.Items[id] = idCache:get(ActiveInstance.Id, id) end -- Apply any custom ID to the current item. local result = ActiveInstance.Items[id] if #IDStack > 0 then result = result .. IDStack[#IDStack] end return result end return nil end function Window.GetLastItem() if ActiveInstance ~= nil then return ActiveInstance.LastItem end return nil end function Window.Validate() if #PendingStack > 1 then assert(false, "EndWindow was not called for: " .. PendingStack[1].Id) end MovingInstance = nil local shouldUpdate = false for i = #Stack, 1, -1 do if Stack[i].IsMoving then MovingInstance = Stack[i] end if Stack[i].FrameNumber ~= Stats.GetFrameNumber() then Stack[i].StackIndex = 0 Region.ClearHotInstance(Stack[i].Id) Region.ClearHotInstance(Stack[i].TitleId) remove(Stack, i) shouldUpdate = true end end if shouldUpdate then UpdateStackIndex() end end function Window.HasResized() if ActiveInstance ~= nil then return ActiveInstance.HasResized end return false end function Window.SetStackLock(id) StackLockId = id end function Window.PushToTop(id) local instance = GetInstance(id) if instance ~= nil then PushToTop(instance) end end function Window.IsAppearing() if ActiveInstance ~= nil then return ActiveInstance.IsAppearing end return false end function Window.GetLayer() if ActiveInstance ~= nil then return ActiveInstance.Layer end return 'Normal' end function Window.GetInstanceIds() local result = {} for i, v in ipairs(Instances) do insert(result, v.Id) end return result end function Window.GetInstanceInfo(id) local result = {} local instance = nil for i, v in ipairs(Instances) do if v.Id == id then instance = v break end end insert(result, "MovingInstance: " .. (MovingInstance ~= nil and MovingInstance.Id or "nil")) if instance ~= nil then insert(result, "Title: " .. instance.Title) insert(result, "TitleH: " .. instance.TitleH) insert(result, "X: " .. instance.X) insert(result, "Y: " .. instance.Y) insert(result, "W: " .. instance.W) insert(result, "H: " .. instance.H) insert(result, "ContentW: " .. instance.ContentW) insert(result, "ContentH: " .. instance.ContentH) insert(result, "TitleDeltaX: " .. instance.TitleDeltaX) insert(result, "TitleDeltaY: " .. instance.TitleDeltaY) insert(result, "SizeDeltaX: " .. instance.SizeDeltaX) insert(result, "SizeDeltaY: " .. instance.SizeDeltaY) insert(result, "DeltaContentW: " .. instance.DeltaContentW) insert(result, "DeltaContentH: " .. instance.DeltaContentH) insert(result, "Border: " .. instance.Border) insert(result, "Layer: " .. instance.Layer) insert(result, "Stack Index: " .. instance.StackIndex) insert(result, "AutoSizeWindow: " .. tostring(instance.AutoSizeWindow)) insert(result, "AutoSizeContent: " .. tostring(instance.AutoSizeContent)) insert(result, "Hot Item: " .. tostring(instance.HotItem)) end return result end function Window.GetStackDebug() local result = {} for i, v in ipairs(Stack) do result[i] = tostring(v.StackIndex) .. ": " .. v.Id if v.Id == StackLockId then result[i] = result[i] .. " (Locked)" end end return result end function Window.IsAutoSize() if ActiveInstance ~= nil then return ActiveInstance.AutoSizeWindowW or ActiveInstance.AutoSizeWindowH end return false end function Window.Save(Table) if Table ~= nil then local settings = {} for i, v in ipairs(Instances) do if not v.NoSavedSettings then settings[v.Id] = { X = v.TitleDeltaX, Y = v.TitleDeltaY, W = v.SizeDeltaX, H = v.SizeDeltaY } end end Table['Window'] = settings end end function Window.Load(Table) if Table ~= nil then local settings = Table['Window'] if settings ~= nil then for k, v in pairs(settings) do local instance = GetInstance(k) instance.TitleDeltaX = v.X instance.TitleDeltaY = v.Y instance.SizeDeltaX = v.W instance.SizeDeltaY = v.H end end end end function Window.GetMovingInstance() return MovingInstance end --[[ Allow developers to push/pop a custom ID to the stack. This can help with differentiating between controls with identical IDs i.e. text fields. --]] function Window.PushID(id) if ActiveInstance ~= nil then insert(IDStack, id) end end function Window.PopID() if #IDStack > 0 then return remove(IDStack) end return nil end function Window.ToDock(type) local activeInstance = GetInstance() activeInstance.W = 720 activeInstance.H = 720 Dock.SetPendingWindow(activeInstance, type) Dock.Override() end return Window ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![](https://github.com/flamendless/Slab/blob/master/assets/slab.png) # Slab Slab is an immediate mode GUI toolkit for the Love 2D framework. This library is designed to allow users to easily add this library to their existing Love 2D projects and quickly create tools to enable them to iterate on their ideas quickly. The user should be able to utilize this library with minimal integration steps and is completely written in Lua and utilizes the Love 2D API. No compiled binaries are required and the user will have access to the source so that they may make adjustments that meet the needs of their own projects and tools. Refer to main.lua and SlabTest.lua for example usage of this library. ### Usage Integrating this library into existing projects is very simple. ```lua local Slab = require 'Slab' function love.load(args) love.graphics.setBackgroundColor(0.4, 0.88, 1.0) Slab.Initialize(args) end function love.update(dt) Slab.Update(dt) Slab.BeginWindow('MyFirstWindow', {Title = "My First Window"}) Slab.Text("Hello World") Slab.EndWindow() end function love.draw() Slab.Draw() end ``` ![](https://github.com/coding-jackalope/Slab/wiki/Images/Slab_HelloWorld.png) For more detailed information on usage of this library, refer to the [Wiki](https://github.com/coding-jackalope/Slab/wiki). [LOVE forum](https://love2d.org/forums/viewtopic.php?t=86410) ### License Slab is licensed under the MIT license. Please see the LICENSE file for more information. ### Credits * [coding-jackalope](https://github.com/coding-jackalope) original developer of this library. * [Dear ImGui](https://github.com/ocornut/imgui) project built by Omar Cornut and various contributors. This project was the inspiration for building an Immediate Mode GUI for Love2D specifically. If anyone is building a game or application in C++, I highly recommend using this library and its rich toolset to speed up development. * [Kenney.nl](https://kenney.nl/) and the [Tango Desktop Project](https://opengameart.org/content/tango-desktop-icons) for providing icons used in this project. * [lovefs](https://github.com/linux-man/lovefs) provides some FFI code for the filesystem. * [luapower/fs](https://github.com/luapower/fs) provides cross platform FFI code for the filesystem. ================================================ FILE: Slab.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] -- This file is for running a project within the Slab folder. This file -- should not be used when using the Slab folder within another project. if SLAB_PATH == nil then SLAB_PATH = (...):match("(.-)[^%.]+$") end ---@type Slab local Slab = require(SLAB_PATH .. '.API') ---@type Slab return Slab ================================================ FILE: SlabDebug.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Slab = require(SLAB_PATH .. '.Slab') local DrawCommands = require(SLAB_PATH .. '.Internal.Core.DrawCommands') local Input = require(SLAB_PATH .. '.Internal.UI.Input') local LayoutManager = require(SLAB_PATH .. '.Internal.UI.LayoutManager') local Mouse = require(SLAB_PATH .. '.Internal.Input.Mouse') local Region = require(SLAB_PATH .. '.Internal.UI.Region') local Stats = require(SLAB_PATH .. '.Internal.Core.Stats') local Style = require(SLAB_PATH .. '.Style') local Tooltip = require(SLAB_PATH .. '.Internal.UI.Tooltip') local Tree = require(SLAB_PATH .. '.Internal.UI.Tree') local Window = require(SLAB_PATH .. '.Internal.UI.Window') local SlabDebug = {} local SlabDebug_About = 'SlabDebug_About' local SlabDebug_Mouse = {Title = "Mouse", IsOpen = false} local SlabDebug_Windows = {Title = "Windows", IsOpen = false} local SlabDebug_Regions = {Title = "Regions", IsOpen = false} local SlabDebug_Tooltip = {Title = "Tooltip", IsOpen = false} local SlabDebug_DrawCommands = {Title = "DrawCommands", IsOpen = false} local SlabDebug_Performance = {Title = "Performance", IsOpen = false} local SlabDebug_StyleEditor = {Title = "Style Editor", IsOpen = false, AutoSizeWindow = false, AllowResize = true, W = 700.0, H = 500.0} local SlabDebug_Input = {Title = "Input", IsOpen = false} local SlabDebug_MultiLine = {Title = "Multi-Line Input", IsOpen = false} local SlabDebug_MultiLine_FileDialog = false local SlabDebug_MultiLine_FileName = "" local SlabDebug_MultiLine_Contents = "" local SlabDebug_Tree = {Title = "Tree", IsOpen = false, AutoSizeWindow = false, AllowResize = true} local SlabDebug_LayoutManager = {Title = "Layout Manager", IsOpen = false, AutoSizeWindow = false, AllowResize = true} local SlabDebug_Windows_Categories = {"Inspector", "Stack"} local SlabDebug_Windows_Category = "Inspector" local SlabDebug_Regions_Selected = "" local Selected_Window = "" local Style_EditingColor = nil local Style_ColorStore = nil local Style_FileDialog = nil local function Window_Inspector() local Ids = Window.GetInstanceIds() if Slab.BeginComboBox('SlabDebug_Windows_Inspector', {Selected = Selected_Window}) then for I, V in ipairs(Ids) do if Slab.TextSelectable(V) then Selected_Window = V end end Slab.EndComboBox() end local Info = Window.GetInstanceInfo(Selected_Window) for I, V in ipairs(Info) do Slab.Text(V) end end local function Window_Stack() local Stack = Window.GetStackDebug() Slab.Text("Stack: " .. #Stack) for I, V in ipairs(Stack) do Slab.Text(V) end end local function DrawCommands_Item(Root, Label) if type(Root) == "table" then if Slab.BeginTree(Label) then for K, V in pairs(Root) do DrawCommands_Item(V, K) end Slab.EndTree() end else Slab.BeginTree(Label .. " " .. tostring(Root), {IsLeaf = true}) end end local DrawPerformance_Category = nil local DrawPerformance_WinX = 50.0 local DrawPerformance_WinY = 50.0 local DrawPerformance_ResetPosition = false local DrawPerformance_Init = false local DrawPerformance_W = 200.0 local function DrawPerformance() if not DrawPerformance_Init then Slab.EnableStats(true) DrawPerformance_Init = true end SlabDebug_Performance.X = DrawPerformance_WinX SlabDebug_Performance.Y = DrawPerformance_WinY SlabDebug_Performance.ResetPosition = DrawPerformance_ResetPosition Slab.BeginWindow('SlabDebug_Performance', SlabDebug_Performance) DrawPerformance_ResetPosition = false local Categories = Stats.GetCategories() if DrawPerformance_Category == nil then DrawPerformance_Category = Categories[1] end if Slab.BeginComboBox('DrawPerformance_Categories', {Selected = DrawPerformance_Category, W = DrawPerformance_W}) then for I, V in ipairs(Categories) do if Slab.TextSelectable(V) then DrawPerformance_Category = V end end Slab.EndComboBox() end if Slab.CheckBox(Slab.IsStatsEnabled(), "Enabled") then Slab.EnableStats(not Slab.IsStatsEnabled()) end Slab.SameLine() if Slab.Button("Flush") then Slab.FlushStats() end Slab.Separator() if DrawPerformance_Category ~= nil then local Items = Stats.GetItems(DrawPerformance_Category) local Pad = 50.0 local MaxW = 0.0 for I, V in ipairs(Items) do MaxW = math.max(MaxW, Slab.GetTextWidth(V)) end local CursorX, CursorY = Slab.GetCursorPos() Slab.SetCursorPos(MaxW * 0.5 - Slab.GetTextWidth("Stat") * 0.5) Slab.Text("Stat") local TimeX = MaxW + Pad local TimeW = Slab.GetTextWidth("Time") local TimeItemW = Slab.GetTextWidth(string.format("%.4f", 0.0)) Slab.SetCursorPos(TimeX, CursorY) Slab.Text("Time") local MaxTimeX = TimeX + TimeW + Pad local MaxTimeW = Slab.GetTextWidth("Max Time") Slab.SetCursorPos(MaxTimeX, CursorY) Slab.Text("Max Time") local CallCountX = MaxTimeX + MaxTimeW + Pad local CallCountW = Slab.GetTextWidth("Call Count") Slab.SetCursorPos(CallCountX, CursorY) Slab.Text("Call Count") DrawPerformance_W = CallCountX + CallCountW Slab.Separator() for I, V in ipairs(Items) do local Time = Stats.GetTime(V, DrawPerformance_Category) local MaxTime = Stats.GetMaxTime(V, DrawPerformance_Category) local CallCount = Stats.GetCallCount(V, DrawPerformance_Category) CursorX, CursorY = Slab.GetCursorPos() Slab.SetCursorPos(MaxW * 0.5 - Slab.GetTextWidth(V) * 0.5) Slab.Text(V) Slab.SetCursorPos(TimeX + TimeW * 0.5 - TimeItemW * 0.5, CursorY) Slab.Text(string.format("%.4f", Time)) Slab.SetCursorPos(MaxTimeX + MaxTimeW * 0.5 - TimeItemW * 0.5, CursorY) Slab.Text(string.format("%.4f", MaxTime)) Slab.SetCursorPos(CallCountX + CallCountW * 0.5 - Slab.GetTextWidth(CallCount) * 0.5, CursorY) Slab.Text(CallCount) end end Slab.EndWindow() end local function EditColor(Color) Style_EditingColor = Color Style_ColorStore = {Color[1], Color[2], Color[3], Color[4]} end local function RestoreEditColor() Style_EditingColor[1] = Style_ColorStore[1] Style_EditingColor[2] = Style_ColorStore[2] Style_EditingColor[3] = Style_ColorStore[3] Style_EditingColor[4] = Style_ColorStore[4] end local function DrawStyleEditor() Slab.BeginWindow('SlabDebug_StyleEditor', SlabDebug_StyleEditor) local X, Y = Slab.GetWindowPosition() local W, H = Slab.GetWindowSize() local Style = Slab.GetStyle() local Names = Style.API.GetStyleNames() local CurrentStyle = Style.API.GetCurrentStyleName() Slab.BeginLayout('SlabDebug_StyleEditor_Styles_Layout', {ExpandW = true}) if Slab.BeginComboBox('SlabDebug_StyleEditor_Styles', {Selected = CurrentStyle}) then for I, V in ipairs(Names) do if Slab.TextSelectable(V) then Style.API.SetStyle(V) end end Slab.EndComboBox() end if Slab.Button("New") then Style_FileDialog = 'new' end Slab.SameLine() if Slab.Button("Load") then Style_FileDialog = 'load' end Slab.SameLine() local SaveDisabled = Style.API.IsDefaultStyle(CurrentStyle) if Slab.Button("Save", {Disabled = SaveDisabled}) then Style.API.SaveCurrentStyle() end Slab.EndLayout() Slab.Separator() local Refresh = false Slab.BeginLayout('SlabDebug_StyleEditor_Content_Layout', {Columns = 2, ExpandW = true}) for K, V in pairs(Style) do if type(V) == "table" and K ~= "Font" and K ~= "API" then Slab.SetLayoutColumn(1) Slab.Text(K) Slab.SetLayoutColumn(2) local W, H = Slab.GetLayoutSize() H = Slab.GetTextHeight() Slab.Rectangle({W = W, H = H, Color = V, Outline = true}) if Slab.IsControlClicked() then if Style_EditingColor ~= nil then RestoreEditColor() Refresh = true end EditColor(V) end end end for K, V in pairs(Style) do if type(V) == "number" and K ~= "FontSize" then Slab.SetLayoutColumn(1) Slab.Text(K) Slab.SetLayoutColumn(2) if Slab.Input('SlabDebug_Style_' .. K, {Text = tostring(V), ReturnOnText = false, NumbersOnly = true}) then Style[K] = Slab.GetInputNumber() end end end Slab.EndLayout() Slab.EndWindow() if Style_EditingColor ~= nil then local Result = Slab.ColorPicker({Color = Style_ColorStore, X = X + W, Y = Y}) Style_EditingColor[1] = Result.Color[1] Style_EditingColor[2] = Result.Color[2] Style_EditingColor[3] = Result.Color[3] Style_EditingColor[4] = Result.Color[4] if Result.Button ~= 0 then if Result.Button == 1 then Style.API.StoreCurrentStyle() end if Result.Button == -1 then RestoreEditColor() end Style_EditingColor = nil end end if Style_FileDialog ~= nil then local Type = Style_FileDialog == 'new' and 'savefile' or Style_FileDialog == 'load' and 'openfile' or nil if Type ~= nil then local Path = love.filesystem.getRealDirectory(SLAB_FILE_PATH) .. "/" .. SLAB_FILE_PATH .. "Internal/Resources/Styles" local Result = Slab.FileDialog({AllowMultiSelect = false, Directory = Path, Type = Type, Filters = {{"*.style", "Styles"}}}) if Result.Button ~= "" then if Result.Button == "OK" then if Style_FileDialog == 'new' then Style.API.CopyCurrentStyle(Result.Files[1]) else Style.API.LoadStyle(Result.Files[1], true) end end Style_FileDialog = nil end else Style_FileDialog = nil end end end function SlabDebug.About() if Slab.BeginDialog(SlabDebug_About, {Title = "About"}) then Slab.Text("Slab Version: " .. Slab.GetVersion()) Slab.Text("Love Version: " .. Slab.GetLoveVersion()) Slab.NewLine() Slab.BeginLayout(SlabDebug_About .. '.Buttons_Layout', {AlignX = 'center'}) if Slab.Button("OK") then Slab.CloseDialog() end Slab.EndLayout() Slab.EndDialog() end end function SlabDebug.OpenAbout() Slab.OpenDialog(SlabDebug_About) end function SlabDebug.Mouse() Slab.BeginWindow('SlabDebug_Mouse', SlabDebug_Mouse) local X, Y = Mouse.Position() Slab.Text("X: " .. X) Slab.Text("Y: " .. Y) local DeltaX, DeltaY = Mouse.GetDelta() Slab.Text("Delta X: " .. DeltaX) Slab.Text("Delta Y: " .. DeltaY) for I = 1, 3, 1 do Slab.Text("Button " .. I .. ": " .. (Mouse.IsDown(I) and "Pressed" or "Released")) end Slab.Text("Hot Region: " .. Region.GetHotInstanceId()) Slab.EndWindow() end function SlabDebug.Windows() Slab.BeginWindow('SlabDebug_Windows', SlabDebug_Windows) if Slab.BeginComboBox('SlabDebug_Windows_Categories', {Selected = SlabDebug_Windows_Category}) then for I, V in ipairs(SlabDebug_Windows_Categories) do if Slab.TextSelectable(V) then SlabDebug_Windows_Category = V end end Slab.EndComboBox() end if SlabDebug_Windows_Category == "Inspector" then Window_Inspector() elseif SlabDebug_Windows_Category == "Stack" then Window_Stack() end Slab.EndWindow() end function SlabDebug.Regions() Slab.BeginWindow('SlabDebug_Regions', SlabDebug_Regions) local Ids = Region.GetInstanceIds() if Slab.BeginComboBox('SlabDebug_Regions_Ids', {Selected = SlabDebug_Regions_Selected}) then for I, V in ipairs(Ids) do if Slab.TextSelectable(V) then SlabDebug_Regions_Selected = V end end Slab.EndComboBox() end local Info = Region.GetDebugInfo(SlabDebug_Regions_Selected) for I, V in ipairs(Info) do Slab.Text(V) end Slab.EndWindow() end function SlabDebug.Tooltip() Slab.BeginWindow('SlabDebug_Tooltip', SlabDebug_Tooltip) local Info = Tooltip.GetDebugInfo() for I, V in ipairs(Info) do Slab.Text(V) end Slab.EndWindow() end function SlabDebug.DrawCommands() Slab.BeginWindow('SlabDebug_DrawCommands', SlabDebug_DrawCommands) local Info = DrawCommands.GetDebugInfo() for K, V in pairs(Info) do DrawCommands_Item(V, K) end Slab.EndWindow() end function SlabDebug.Performance() DrawPerformance() end function SlabDebug.Performance_SetPosition(X, Y) DrawPerformance_WinX = X ~= nil and X or 50.0 DrawPerformance_WinY = Y ~= nil and Y or 50.0 DrawPerformance_ResetPosition = true end function SlabDebug.StyleEditor() DrawStyleEditor() end function SlabDebug.Input() Slab.BeginWindow('SlabDebug_Input', SlabDebug_Input) local Info = Input.GetDebugInfo() Slab.Text("Focused: " .. Info['Focused']) Slab.Text("Width: " .. Info['Width']) Slab.Text("Height: " .. Info['Height']) Slab.Text("Cursor X: " .. Info['CursorX']) Slab.Text("Cursor Y: " .. Info['CursorY']) Slab.Text("Cursor Position: " .. Info['CursorPos']) Slab.Text("Character: " .. Info['Character']) Slab.Text("Line Position: " .. Info['LineCursorPos']) Slab.Text("Line Position Max: " .. Info['LineCursorPosMax']) Slab.Text("Line Number: " .. Info['LineNumber']) Slab.Text("Line Length: " .. Info['LineLength']) local Lines = Info['Lines'] if Lines ~= nil then Slab.Text("Lines: " .. #Lines) end Slab.EndWindow() end local SlabDebug_MultiLine_Highlight = { ['function'] = {1, 0, 0, 1}, ['end'] = {1, 0, 0, 1}, ['if'] = {1, 0, 0, 1}, ['then'] = {1, 0, 0, 1}, ['local'] = {1, 0, 0, 1}, ['for'] = {1, 0, 0, 1}, ['do'] = {1, 0, 0, 1}, ['not'] = {1, 0, 0, 1}, ['while'] = {1, 0, 0, 1}, ['repeat'] = {1, 0, 0, 1}, ['until'] = {1, 0, 0, 1}, ['break'] = {1, 0, 0, 1}, ['else'] = {1, 0, 0, 1}, ['elseif'] = {1, 0, 0, 1}, ['in'] = {1, 0, 0, 1}, ['and'] = {1, 0, 0, 1}, ['or'] = {1, 0, 0, 1}, ['true'] = {1, 0, 0, 1}, ['false'] = {1, 0, 0, 1}, ['nil'] = {1, 0, 0, 1}, ['return'] = {1, 0, 0, 1} } local SlabDebug_MultiLine_ShouldHighlight = true function SlabDebug.MultiLine() Slab.BeginWindow('SlabDebug_MultiLine', SlabDebug_MultiLine) if Slab.Button("Load") then SlabDebug_MultiLine_FileDialog = true end Slab.SameLine() if Slab.Button("Save", {Disabled = SlabDebug_MultiLine_FileName == ""}) then local Handle, Error = io.open(SlabDebug_MultiLine_FileName, "w") if Handle ~= nil then Handle:write(SlabDebug_MultiLine_Contents) Handle:close() end end local ItemW, ItemH = Slab.GetControlSize() Slab.SameLine() if Slab.CheckBox(SlabDebug_MultiLine_ShouldHighlight, "Use Lua Highlight", {Size = ItemH}) then SlabDebug_MultiLine_ShouldHighlight = not SlabDebug_MultiLine_ShouldHighlight end Slab.Separator() Slab.Text("File: " .. SlabDebug_MultiLine_FileName) if Slab.Input('SlabDebug_MultiLine', { MultiLine = true, Text = SlabDebug_MultiLine_Contents, W = 500.0, H = 500.0, Highlight = SlabDebug_MultiLine_ShouldHighlight and SlabDebug_MultiLine_Highlight or nil }) then SlabDebug_MultiLine_Contents = Slab.GetInputText() end Slab.EndWindow() if SlabDebug_MultiLine_FileDialog then local Result = Slab.FileDialog({AllowMultiSelect = false, Type = 'openfile'}) if Result.Button ~= "" then SlabDebug_MultiLine_FileDialog = false if Result.Button == "OK" then SlabDebug_MultiLine_FileName = Result.Files[1] local Handle, Error = io.open(SlabDebug_MultiLine_FileName, "r") if Handle ~= nil then SlabDebug_MultiLine_Contents = Handle:read("*a") Handle:close() end end end end end function SlabDebug.Tree() if not SlabDebug_Tree.IsOpen then return end local Info = Tree.GetDebugInfo() Slab.BeginWindow('Tree', SlabDebug_Tree) Slab.Text("Instances: " .. #Info) Slab.BeginLayout('Tree_List_Layout', {ExpandW = true, ExpandH = true}) Slab.BeginListBox('Tree_List') for I, V in ipairs(Info) do Slab.BeginListBoxItem('Item_' .. I) Slab.Text(V) Slab.EndListBoxItem() end Slab.EndListBox() Slab.EndLayout() Slab.EndWindow() end local SlabDebug_LayoutManager_Selected = nil function SlabDebug.LayoutManager() local Info = LayoutManager.GetDebugInfo() Slab.BeginWindow('LayoutManager', SlabDebug_LayoutManager) Slab.BeginLayout('LayoutManager_Layout', {ExpandW = true}) if Slab.BeginComboBox('LayoutManager_ID', {Selected = SlabDebug_LayoutManager_Selected}) then for K, V in pairs(Info) do if SlabDebug_LayoutManager_Selected == nil then SlabDebug_LayoutManager_Selected = K end if Slab.TextSelectable(K) then SlabDebug_LayoutManager_Selected = K end end Slab.EndComboBox() end Slab.EndLayout() if SlabDebug_LayoutManager_Selected ~= nil then local Items = Info[SlabDebug_LayoutManager_Selected] for I, V in ipairs(Items) do Slab.Text(V) end end Slab.EndWindow() end local function MenuItemWindow(Options) if Slab.MenuItemChecked(Options.Title, Options.IsOpen) then Options.IsOpen = not Options.IsOpen end end function SlabDebug.Menu() if Slab.BeginMenu("Debug") then if Slab.MenuItem("About") then SlabDebug.OpenAbout() end MenuItemWindow(SlabDebug_Mouse) MenuItemWindow(SlabDebug_Windows) MenuItemWindow(SlabDebug_Regions) MenuItemWindow(SlabDebug_Tooltip) MenuItemWindow(SlabDebug_DrawCommands) MenuItemWindow(SlabDebug_Performance) MenuItemWindow(SlabDebug_StyleEditor) MenuItemWindow(SlabDebug_Input) MenuItemWindow(SlabDebug_MultiLine) MenuItemWindow(SlabDebug_Tree) MenuItemWindow(SlabDebug_LayoutManager) Stats.SetEnabled(SlabDebug_Performance.IsOpen) Slab.EndMenu() end end function SlabDebug.Begin() SlabDebug.About() SlabDebug.Mouse() SlabDebug.Windows() SlabDebug.Regions() SlabDebug.Tooltip() SlabDebug.DrawCommands() SlabDebug.Performance() SlabDebug.StyleEditor() SlabDebug.Input() SlabDebug.MultiLine() SlabDebug.Tree() SlabDebug.LayoutManager() end return SlabDebug ================================================ FILE: SlabDefinition.lua ================================================ ---@meta ---@class Slab local Slab = {} ---@class Slab.BeginWindowOptions ---@field X? number ---@field Y? number ---@field W? number ---@field H? number ---@field ContentW? number ---@field ContentH? number ---@field BgColor? table ---@field Title? string ---@field TitleH? number ---@field TitleAlignX? "left" | "center" | "right" ---@field TitleAlignY? "top" | "center" | "bottom" ---@field AllowMove? boolean ---@field AllowResize? boolean ---@field AllowFocus? boolean ---@field Border? number ---@field NoOutline? boolean ---@field IsMenuBar? boolean ---@field AutoSizeWindow? boolean ---@field AutoSizeWindowW? boolean ---@field AutoSizeWindowH? boolean ---@field AutoSizeContent? boolean ---@field Layer? string ---@field ResetPosition? boolean ---@field ResetSize? boolean ---@field ResetContent? boolean ---@field ResetLayout? boolean ---@field SizerFilter? table ---@field CanObstruct? boolean ---@field Rounding? number ---@field IsOpen? boolean ---@field NoSaveSettings? boolean ---@field ConstrainPosition? boolean ---@field ShowMinimize? boolean ---@field ShowScrollbarX? boolean ---@field ShowScrollbarY? boolean ---@param id string ---@param options? Slab.BeginWindowOptions ---@return boolean Slab.BeginWindow = function(id, options) end Slab.EndWindow = function() end ---@return number, number Slab.GetWindowPosition = function() end ---@return number, number Slab.GetWindowContentSize = function() end ---@return number, number Slab.GetWindowActiveSize = function() end ---@return boolean Slab.IsWindowAppearing = function() end ---@param id string Slab.PushID = function(id) end Slab.PopID = function() end ---@return table Slab.GetStyle = function() end ---@param font love.graphics.Font Slab.PushFont = function(font) end Slab.PopFont = function() end ---@param path string ---@param set boolean ---@return table Slab.LoadStyle = function(path, set) end ---@param name string ---@return boolean Slab.SetStyle = function(name) end ---@return table Slab.GetStyleNames = function() end ---@return string Slab.GetCurrentStyleName = function() end ---@return boolean Slab.BeginMainMenuBar = function() end Slab.EndMainMenuBar = function() end ---@param isMainMenuBar boolean ---@return boolean Slab.BeginMenuBar = function(isMainMenuBar) end ---@class Slab.BeginMenuOptions ---@field Enabled? boolean ---@param label string ---@param options? Slab.BeginMenuOptions ---@return boolean Slab.BeginMenu = function(label, options) end Slab.EndMenu = function() end ---@param button number ---@return boolean Slab.BeginContextMenuItem = function(button) end ---@param button number ---@return boolean Slab.BeginContextMenuWindow = function(button) end Slab.EndContextMenu = function() end ---@class Slab.MenuItemOptions ---@field Enabled? boolean ---@param label string ---@param options? Slab.MenuItemOptions Slab.MenuItem = function(label, options) end ---@class Slab.MenuItemCheckedOptions ---@field Enabled? boolean ---@param label string ---@param isChecked boolean ---@param options? Slab.MenuItemCheckedOptions ---@return boolean Slab.MenuItemChecked = function(label, isChecked, options) end ---@param id string Slab.OpenDialog = function(id) end ---@param id string ---@param options Slab.BeginWindowOptions Slab.BeginDialog = function(id, options) end Slab.EndDialog = function() end Slab.CloseDialog = function() end ---@class Slab.MessageBoxOptions ---@field Button? table ---@param title string ---@param message string ---@param options? Slab.MessageBoxOptions ---@return string Slab.MessageBox = function(title, message, options) end ---@class Slab.FileDialogOptions ---@field Directry? string ---@field Type? string ---@field Filters? table ---@field IncludeParent? boolean ---@param options Slab.FileDialogOptions ---@return table Slab.FileDialog = function(options) end ---@class Slab.ColorPickerOptions ---@field Color? table ---@param options Slab.ColorPickerOptions ---@return table Slab.ColorPicker = function(options) end ---@param list string | table Slab.EnableDocks = function(list) end ---@param list string | table Slab.DisableDocks = function(list) end ---@class Slab.SetDockOptionsOptiond ---@field NoSaveSettings? boolean ---@param options Slab.SetDockOptionsOptiond Slab.SetDockOptions = function(options) end ---@param type string Slab.WindowToDock = function(type) end ---@class Slab.ButtonOptions ---@field Tooltip? string ---@field Rounding? number ---@field Invisible? boolean ---@field W? number ---@field H? number ---@field Disable? boolean ---@field Image? table ---@field Color? table ---@field HoverColor? table ---@field PressColor? table ---@field PadX? number ---@field PadY? number ---@field VLines? number ---@param label string ---@param options? Slab.ButtonOptions ---@return boolean Slab.Button = function(label, options) end ---@class Slab.RadioButtonOptions ---@field Index? number ---@field SelectedInxex? number ---@field Tooltip? string ---@param label string ---@param options? Slab.RadioButtonOptions ---@return boolean Slab.RadioButton = function(label, options) end ---@class Slab.TextOptions ---@field Color? table ---@field Pad? number ---@field IsSelectable? boolean ---@field IsSelectableTextOnly? boolean ---@field IsSelected? boolean ---@field SleectOnHover? boolean ---@field HoverColor? table ---@param label string ---@param options? Slab.TextOptions ---@return boolean Slab.Text = function(label, options) end ---@param label string ---@param options Slab.TextOptions ---@return boolean Slab.TextSelectable = function(label, options) end ---@class Slab.TextfOptions ---@field Color table ---@field W number ---@field Align "left" | "center" | "right" ---@param label string ---@param options Slab.TextOptions Slab.Textf = function(label, options) end ---@param label string ---@return number, number Slab.GetTextSize = function(label) end ---@param label string ---@return number Slab.GetTextWidth = function(label) end ---@return number Slab.GetTextHeight = function() end ---@class Slab.CheckBoxOptions ---@field Tooltip? string ---@field Id? string ---@field Rounding? number ---@field Size? number ---@field Disable? boolean ---@param enabled boolean ---@param label string ---@param options? Slab.CheckBoxOptions ---@return boolean Slab.CheckBox = function(enabled, label, options) end ---@class Slab.InputOptions ---@field Tooltip? string ---@field ReturnOnTexy? boolean ---@field Text? string | number ---@field TextColor? table ---@field BgColor? table ---@field SelectColor? table ---@field SelectOnFocus? boolean ---@field NumbersOnly? boolean ---@field W? number ---@field H? number ---@field ReadOnly? boolean ---@field Align? "left" | "center" | "right" ---@field Rounding? number ---@field MinNumber? number ---@field MaxNumber? number ---@field MultiLine? boolean ---@field MultiLineW? number ---@field Highlight? table ---@field Step? number ---@field NoDrag? boolean ---@field UseSlider? boolean ---@field IsPassword? boolean ---@field PasswordChar? string ---@param id string ---@param options? Slab.InputOptions ---@return boolean Slab.Input = function(id, options) end ---@param id string ---@param value number ---@param min number ---@param max number ---@param step number ---@param options Slab.InputOptions Slab.InputNumberDrag = function(id, value, min, max, step, options) end ---@return string Slab.GetInputText = function() end ---@return number Slab.GetInputNumber = function() end ---@return number, number, number Slab.GetInputCursorPos = function() end ---@param id string ---@return boolean Slab.IsInputFocused = function(id) end ---@return boolean Slab.IsAnyInputFocused = function() end ---@param id string Slab.SetInputFocus = function(id) end ---@param pos number Slab.SetInputCursorPos = function(pos) end ---@param column number ---@param line number Slab.SetInputCursorPosLine = function(column, line) end ---@class Slab.BeginTreeOptions ---@field Label? string ---@field Tooltip? string ---@field IsLeaf? boolean ---@field OpenWithHighlight? boolean ---@field icon? table ---@field IsSelected? boolean ---@field IsOpen? boolean ---@field NoSaveSettings? boolean ---@param id string ---@param options? Slab.BeginTreeOptions ---@return boolean Slab.BeginTree = function(id, options) end Slab.EndTree = function() end ---@class Slab.BeginComboBoxOptions ---@field Tooltip? string ---@field Selected? string ---@field W? number ---@field Rounding? number ---@param id string ---@param options? Slab.BeginComboBoxOptions ---@return boolean Slab.BeginComboBox = function(id, options) end Slab.EndComboBox = function() end ---@class Slab.ImageOptions ---@field Image? love.graphics.Image ---@field Path? string ---@field Rotation? number ---@field Scale? number ---@field ScaleY? number ---@field ScaleX? number ---@field Color? table ---@field SubX? number ---@field SubY? number ---@field SubW? number ---@field SubH? number ---@field WrapX? string ---@field WrapY? string ---@field UseOutline? boolean ---@field OutlineColor? table ---@field OutlineWidth? number ---@field W? number ---@field H? number ---@param id string ---@param options? Slab.ImageOptions Slab.Image = function(id, options) end ---@class Slab.BeginListBoxOptions ---@field W? number ---@field H? number ---@field Clear? boolean ---@field Rounding? number ---@field StretchW? boolean ---@field StretchH? boolean ---@param id string ---@param options? Slab.BeginListBoxOptions Slab.BeginListBox = function(id, options) end Slab.EndListBox = function() end ---@class Slab.BeginListBoxItemOptions ---@field Selected? boolean ---@param id string ---@param options? Slab.BeginListBoxItemOptions Slab.BeginListBoxItem = function(id, options) end ---@param button number ---@param isDoubleClick boolean Slab.IsListBoxItemClicked = function(button, isDoubleClick) end Slab.EndListBoxItem = function() end ---@class Slab.RectangleOptions ---@field Mode? string ---@field W? number ---@field H? number ---@field Color? table ---@field Rounding? number | table ---@field Outline? boolean ---@field OutlineColor? table ---@field Segment? number ---@param options? Slab.RectangleOptions Slab.Rectangle = function(options) end ---@class Slab.CircleOptions ---@field Mode? string ---@field Radius? number ---@field Color? table ---@field Segment? number ---@param options? Slab.CircleOptions Slab.Circle = function(options) end ---@class Slab.TriangleOptions ---@field Mode? string ---@field Radius? number ---@field Rotation? number ---@field Color? table ---@param options? Slab.TriangleOptions Slab.Triangle = function(options) end ---@class Slab.LineOptions ---@field Width? number ---@field Color? table ---@param x2 number ---@param y2 number ---@param options? Slab.LineOptions Slab.Line = function(x2, y2, options) end ---@class Slab.CurveOptions ---@field Color? table ---@field Depth? number ---@param point table ---@param options? Slab.CurveOptions Slab.Curve = function(point, options) end ---@return number Slab.GetCurveControlPointCount = function() end ---@return number, number Slab.GetCurveControlPoint = function() end ---@class Slab.EvaluateCurveOptions ---@field LocalSpace? boolean ---@param time number ---@param options? Slab.EvaluateCurveOptions ---@return number, number Slab.EvaluateCurve = function(time, options) end ---@param options? Slab.EvaluateCurveOptions ---@return number, number Slab.EvaluateCurveMouse = function(options) end ---@class Slab.PolygonOptions ---@field Color? table ---@field Mode? string ---@param points table ---@param options? Slab.PolygonOptions Slab.Polygon = function(points, options) end ---@param number number Slab.SetScrollSpeed = function(number) end ---@return number Slab.GetScrollSpeed = function() end ---@class Slab.SeparatorOptions ---@field IncudeBorders? boolean ---@field H? number ---@field Thickness? number ---@param options? Slab.SeparatorOptions Slab.Separator = function(options) end ---@param shader love.graphics.Shader Slab.PushShader = function(shader) end Slab.PopShader = function() end ---@param table table ---@param options table ---@param fallback table Slab.Properties = function(table, options, fallback) end ---@param n? number Slab.NewLine = function(n) end ---@class Slab.SameLineOptions ---@field Pad? number ---@field CenterY? boolean ---@param options? Slab.SameLineOptions Slab.SameLine = function(options) end ---@class Slab.SetCursorPosOptions ---@field Absolute? boolean ---@param x number ---@param y number ---@param options Slab.SetCursorPosOptions Slab.SetCursorPos = function(x, y, options) end ---@param width number Slab.Indent = function(width) end ---@param width number Slab.UnIndent = function(width) end ---@param button number | 1 | 2 | 3 ---@return boolean Slab.IsMouseDown = function(button) end ---@param button number | 1 | 2 | 3 ---@return boolean Slab.IsMouseClicked = function(button) end ---@param button number | 1 | 2 | 3 ---@return boolean Slab.IsMouseReleased = function(button) end ---@param button number | 1 | 2 | 3 ---@return boolean Slab.IsMouseDoubleClicked = function(button) end ---@param button number | 1 | 2 | 3 ---@return boolean Slab.IsMouseDragging = function(button) end ---@return number, number Slab.GetMousePosition = function() end ---@return number, number Slab.GetMousePositionWindow = function() end ---@return number, number Slab.GetMouseDelta = function() end ---@alias CursorCustomType ---| "arrow" ---| "sizewe" ---| "sizens" ---| "sizenesw" ---| "ibeam" ---| "hand" ---@param type CursorCustomType ---@param image love.graphics.Image ---@param quad love.graphics.Quad Slab.SetCustomMouseCursor = function(type, image, quad) end ---@param type CursorCustomType Slab.ClearCustomMouseCursor = function(type) end ---@return boolean Slab.IsControlHovered = function() end ---@param button number | 1 | 2 | 3 ---@return boolean Slab.IsControlClicked = function(button) end ---@return number, number Slab.GetControlSize = function() end ---@return boolean Slab.IsVoidHovered = function() end ---@param button number | 1 | 2 | 3 ---@return boolean Slab.IsVoidClicked = function(button) end ---@param key string ---@return boolean Slab.IsKeyDown = function(key) end ---@param key string ---@return boolean Slab.IsKeyPressed = function(key) end ---@param key string ---@return boolean Slab.IsKeyReleased = function(key) end ---@class Slab.BeginLayoutOptions ---@field AlignX? "left" | "center" | "right" ---@field AlignY? "top" | "center" | "botton" ---@field AlignRowY? "top" | "center" | "botton" ---@field Ignore? boolean ---@field ExpandW? boolean ---@field ExpandH? boolean ---@field AnchorX? boolean ---@field AnchorT? boolean ---@field Columns? number ---@param id string ---@param options? Slab.BeginLayoutOptions Slab.BeginLayout = function(id, options) end Slab.EndLayout = function() end ---@param index number Slab.SetLayoutColumn = function(index) end ---@return number, number Slab.GetLayoutSize = function() end ---@return number Slab.GetCurrentColumnIndex = function() end ---@param args table Slab.Initialize = function(args) end ---@return string Slab.GetVersion = function() end ---@return string Slab.GetLoveVersion = function() end ---@param dt number Slab.Update = function(dt) end Slab.Draw = function() end ---@param isVerbose boolean Slab.SetVerbose = function(isVerbose) end ---@param name string ---@param category string ---@return number Slab.BeginStat = function(name, category) end ---@param number number Slab.EndStat = function(number) end ---@param enable boolean Slab.EnableStats = function(enable) end ---@return boolean Slab.IsStatsEnabled = function() end ---@return table Slab.GetStats = function() end ---@param loveStats table ---@return table Slab.CalculateStats = function(loveStats) end ---@param path string Slab.SetINIStatePath = function(path) end ---@return string Slab.GetINIStatePath = function() end ---@return table Slab.GetMassages = function() end ================================================ FILE: SlabTest.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Slab = require(SLAB_PATH .. '.Slab') local SlabDebug = require(SLAB_PATH .. '.SlabDebug') local SlabTest = {} local function DrawOverview() Slab.Textf( "Slab is an immediate mode GUI toolkit for the LÖVE 2D framework. This library " .. "is designed to allow users to easily add this library to their existing LÖVE 2D projects and " .. "quickly create tools to enable them to iterate on their ideas quickly. The user should be able " .. "to utilize this library with minimal integration steps and is completely written in Lua and utilizes " .. "the LÖVE 2D API. No compiled binaries are required and the user will have access to the source so " .. "that they may make adjustments that meet the needs of their own projects and tools. Refer to main.lua " .. "and SlabTest.lua for example usage of this library.\n\n" .. "This window will demonstrate the usage of the Slab library and give an overview of all the supported controls " .. "and features.") Slab.NewLine() Slab.Text("The current version of Slab is: ") Slab.SameLine() Slab.Text(Slab.GetVersion(), {Color = {0, 1, 0, 1}}) Slab.Text("The current version of LÖVE is: ") Slab.SameLine() Slab.Text(Slab.GetLoveVersion(), {Color = {0, 1, 0, 1}}) Slab.Text("The current OS is: ") Slab.SameLine() Slab.Text(love.system.getOS(), {Color = {0, 1, 0, 1}}) end local DrawButtons_NumClicked = 0 local DrawButtons_NumClicked_Invisible = 0 local DrawButtons_Enabled = false local DrawButtons_Hovered = false local function DrawButtons() Slab.Textf("Buttons are simple controls which respond to a user's left mouse click. Buttons will simply return true when they are clicked.") Slab.NewLine() if Slab.Button("Button") then DrawButtons_NumClicked = DrawButtons_NumClicked + 1 end Slab.SameLine() Slab.Text("You have clicked this button " .. DrawButtons_NumClicked .. " time(s).") Slab.NewLine() Slab.Separator() Slab.Textf("Buttons can be tested for mouse hover with the call to Slab.IsControlHovered right after declaring the button.") Slab.Button(DrawButtons_Hovered and "Hovered" or "Not Hovered", {W = 100}) DrawButtons_Hovered = Slab.IsControlHovered() Slab.NewLine() Slab.Separator() Slab.Textf("Buttons can have a custom width and height.") Slab.Button("Square", {W = 75, H = 75}) Slab.NewLine() Slab.Separator() Slab.Textf( "Buttons can also be invisible so that the designer can implement a custom button but still rely on the " .. "button behavior. Below is a an invisible button and a custom rectangle drawn at the same location.") local X, Y = Slab.GetCursorPos() Slab.Rectangle({Mode = 'line', W = 50.0, H = 50.0, Color = {1, 1, 1, 1}}) Slab.SetCursorPos(X, Y) if Slab.Button("", {Invisible = true, W = 50.0, H = 50.0}) then DrawButtons_NumClicked_Invisible = DrawButtons_NumClicked_Invisible + 1 end Slab.SameLine({CenterY = true}) Slab.Text("Invisible button has been clicked " .. DrawButtons_NumClicked_Invisible .. " time(s).") Slab.NewLine() Slab.Separator() Slab.Textf("Buttons can also be disabled. Click the button below to toggle the status of the neighboring button.") if Slab.Button("Toggle", { Active = DrawButtons_Enabled }) then DrawButtons_Enabled = not DrawButtons_Enabled end Slab.SameLine() Slab.Button(DrawButtons_Enabled and "Enabled" or "Disabled", {Disabled = not DrawButtons_Enabled}) Slab.NewLine() Slab.Separator() Slab.Textf( "Buttons can also display images instead of a text label. This can be down through the 'Image' option, which accepts a table " .. "where the options are the same as those found in the 'Image' API function.") Slab.Button("", {Image = {Path = SLAB_FILE_PATH .. "/Internal/Resources/Textures/avatar.png"}}) end local DrawText_Width = 450.0 local DrawText_Height = 0.0 local DrawText_Alignment = {'left', 'center', 'right', 'justify'} local DrawText_Alignment_Selected = 'left' local DrawText_NumClicked = 0 local DrawText_NumClicked_TextOnly = 0 local function DrawText() Slab.Textf("Text controls displays text on the current window. Slab currently offers three ways to control the text.") Slab.NewLine() Slab.Separator() Slab.Text("The most basic text control is Slab.Text.") Slab.Text("The color of the text can be controlled with the 'Color' option.", {Color = {0, 1, 0, 1}}) Slab.NewLine() Slab.Separator() Slab.Textf( "Text can be formatted using the Slab.Textf API. Formatted text will wrap the text based on the 'W' option. " .. "If the 'W' option is not specified, the window's width will be used as the width. Formatted text also has an " .. "alignment option. The 'H' option can be used to center the text within a given height.") Slab.NewLine() Slab.Text("Width") Slab.SameLine() if Slab.Input('DrawText_Width', {Text = tostring(DrawText_Width), NumbersOnly = true, ReturnOnText = false}) then DrawText_Width = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Height") Slab.SameLine() if Slab.Input('DrawText_Height', {Text = tostring(DrawText_Height), NumbersOnly = true, ReturnOnText = false}) then DrawText_Height = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Alignment") Slab.SameLine() if Slab.BeginComboBox('DrawText_Alignment', {Selected = DrawText_Alignment_Selected}) then for I, V in ipairs(DrawText_Alignment) do if Slab.TextSelectable(V) then DrawText_Alignment_Selected = V end end Slab.EndComboBox() end Slab.Textf( "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore " .. "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " .. "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " .. "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " .. "officia deserunt mollit anim id est laborum.", {W = DrawText_Width, H = DrawText_Height, Align = DrawText_Alignment_Selected}) Slab.NewLine() Slab.Separator() Slab.Textf( "Text can also be interacted with using the Slab.TextSelectable function. A background will be " .. "rendered when the mouse is hovered over the text and the function will return true when clicked on. " .. "The selectable area expands to the width of the window by default. This can be changed to just the text " .. "with the 'IsSelectableTextOnly' option.") Slab.NewLine() if Slab.TextSelectable("This text has been clicked " .. DrawText_NumClicked .. " time(s).") then DrawText_NumClicked = DrawText_NumClicked + 1 end Slab.NewLine() if Slab.TextSelectable("This text has been clicked " .. DrawText_NumClicked_TextOnly .. " time(s).", {IsSelectableTextOnly = true}) then DrawText_NumClicked_TextOnly = DrawText_NumClicked_TextOnly + 1 end Slab.NewLine() Slab.Separator() Slab.Textf( "Text controls can be configured to contain URL links. When this control is clicked, Slab will open the given URL " .. "with the user's default web browser.") Slab.Text("Love 2D", {URL = "http://love2d.org"}) end local DrawCheckBox_Checked = false local DrawCheckBox_Checked_NoLabel = false local function DrawCheckBox() Slab.Textf( "Check boxes are controls that will display an empty box with an optional label. The function will " .. "return true if the user has clicked on the box. The code is then responsible for updating the checked " .. "flag to be passed back into the function.") Slab.NewLine() if Slab.CheckBox(DrawCheckBox_Checked, "Check Box") then DrawCheckBox_Checked = not DrawCheckBox_Checked end Slab.NewLine() Slab.Text("A check box with no label.") if Slab.CheckBox(DrawCheckBox_Checked_NoLabel) then DrawCheckBox_Checked_NoLabel = not DrawCheckBox_Checked_NoLabel end Slab.NewLine() Slab.Text("A disabled check box.") Slab.CheckBox(true, "Disabled", {Disabled = true}) end local DrawRadioButton_Selected = 1 local function DrawRadioButton() Slab.Textf("Radio buttons offer the user to select one option from a list of options.") Slab.NewLine() for I = 1, 5, 1 do if Slab.RadioButton("Option " .. I, {Index = I, SelectedIndex = DrawRadioButton_Selected}) then DrawRadioButton_Selected = I end end end local DrawMenus_Window_Selected = "Right click and select an option." local DrawMenus_Control_Selected = "Right click and select an option from a control." local DrawMenus_CheckBox = false local DrawMenus_ComboBox = {"Apple", "Banana", "Pear", "Orange", "Lemon"} local DrawMenus_ComboBox_Selected = "Apple" local function DrawContextMenuItem(Label, Button) if Slab.BeginContextMenuItem(Button) then for I = 1, 5, 1 do local MenuLabel = Label .. " Option " .. I if Slab.MenuItem(MenuLabel) then DrawMenus_Control_Selected = MenuLabel end end Slab.EndContextMenu() end end local function DrawMenus() Slab.Textf( "Menus are windows that allow users to make a selection from a list of items. Items can be disabled to prevent " .. "any interaction but will still be displayed. Below are descriptions of the various menus and how they can be utilized.") Slab.NewLine() Slab.Separator() Slab.Textf( "The main menu bar is rendered at the top of the window with menu items being added " .. "from left to right. When a menu item is clicked, a context menu is opened below the " .. "selected item. Creating the main menu bar can open anywhere in the code after the " .. "Slab.Update call. These functions should not be called within a BeginWindow/EndWindow " .. "call.") Slab.NewLine() Slab.Separator() Slab.Textf( "Context menus are menus which are rendered above all other controls to allow the user to make a selection " .. "out of a list of items. These can be opened up through the menu bar, or through a right-click " .. "action from the user on a given window or control. Menus and menu items make up the context menu " .. "and menus can be nested to allow a tree options to be displayed.") Slab.NewLine() Slab.Textf( "Controls can have their own context menus. Right-click on each control to open up the menu " .. "and select an option.") Slab.NewLine() Slab.Text(DrawMenus_Control_Selected) Slab.NewLine() Slab.Button("Button") DrawContextMenuItem("Button") Slab.Text("Text") DrawContextMenuItem("Text") if Slab.CheckBox(DrawMenus_CheckBox, "Check Box") then DrawMenus_CheckBox = not DrawMenus_CheckBox end DrawContextMenuItem("Check Box") Slab.Input('DrawMenus_Input') DrawContextMenuItem("Input") if Slab.BeginComboBox('DrawMenus_ComboBox', {Selected = DrawMenus_ComboBox_Selected}) then for I, V in ipairs(DrawMenus_ComboBox) do if Slab.TextSelectable(V) then DrawMenus_Window_Selected = V end end Slab.EndComboBox() end DrawContextMenuItem("Combo Box") Slab.NewLine() Slab.Textf( "Context menu items are usually opened with the right mouse button. This can be changed for context menus to be a differen " .. "mouse button. The button below will open a context menu using the left mouse button.") Slab.NewLine() Slab.Button("Left Mouse") DrawContextMenuItem("Left Mouse Button", 1) Slab.NewLine() Slab.Separator() Slab.Textf( "Right-clicking anywhere within this window will open up a context menu. Note that BeginContextMenuWindow " .. "must come after all BeginContextMenuItem calls.") Slab.NewLine() Slab.Textf(DrawMenus_Window_Selected) if Slab.BeginContextMenuWindow() then if Slab.BeginMenu("Window Menu 1") then for I = 1, 5, 1 do local Enabled = I % 2 ~= 0 if Slab.MenuItem("Sub Window Option " .. I, {Enabled = Enabled}) then DrawMenus_Window_Selected = "Sub Window Option " .. I end end Slab.EndMenu() end for I = 1, 5, 1 do if Slab.MenuItem("Window Option " .. I) then DrawMenus_Window_Selected = "Window Option " .. I .. " selected." end end Slab.EndContextMenu() end end local DrawComboBox_Options = {"England", "France", "Germany", "USA", "Canada", "Mexico", "Japan", "South Korea", "China", "Russia", "India"} local DrawComboBox_Selected = "USA" local DrawComboBox_Selected_Width = "USA" local function DrawComboBox() Slab.Textf( "A combo box allows the user to select a single item from a list and display the selected item " .. "in the combo box. The list is only visible when the user is interacting with the control.") Slab.NewLine() if Slab.BeginComboBox('DrawComboBox_One', {Selected = DrawComboBox_Selected}) then for I, V in ipairs(DrawComboBox_Options) do if Slab.TextSelectable(V) then DrawComboBox_Selected = V end end Slab.EndComboBox() end Slab.NewLine() Slab.Separator() Slab.Textf("A combo box's width can be modified with the 'W' option.") Slab.NewLine() local W, H = Slab.GetWindowActiveSize() if Slab.BeginComboBox('DrawComboBox_Two', {Selected = DrawComboBox_Selected_Width, W = W}) then for I, V in ipairs(DrawComboBox_Options) do if Slab.TextSelectable(V) then DrawComboBox_Selected_Width = V end end Slab.EndComboBox() end end local DrawInput_Basic = "Hello World" local DrawInput_Basic_Return = "Hello World" local DrawInput_Basic_Numbers = 0 local DrawInput_Basic_Numbers_Clamped = 0.5 local DrawInput_Basic_Numbers_Clamped_Min = 0.0 local DrawInput_Basic_Numbers_Clamped_Max = 1.0 local DrawInput_Basic_Numbers_Clamped_Step = 0.01 local DrawInput_Basic_Numbers_NoDrag = 50 local DrawInput_Basic_Numbers_Slider = 50 local DrawInput_Basic_Numbers_Slider_Handle = 50 local DrawInput_Basic_Numbers_Slider_Min = 0 local DrawInput_Basic_Numbers_Slider_Max = 100 local DrawInput_MultiLine = [[ function Foo() print("Bar") end The quick brown fox jumped over the lazy dog.]] local DrawInput_MultiLine_Width = math.huge local DrawInput_CursorPos = 0 local DrawInput_CursorColumn = 0 local DrawInput_CursorLine = 0 local DrawInput_Highlight_Text = [[ function Hello() print("World") end]] local DrawInput_Highlight_Table = { ['function'] = {1, 0, 0, 1}, ['end'] = {0, 0, 1, 1} } local DrawInput_Highlight_Table_Modify = nil local function DrawInput() Slab.Textf( "The input control allows the user to enter in text into an input box. This control is similar " .. "to input boxes found in other applications. These controls are set up to handle UTF8 characters.") Slab.NewLine() Slab.Textf( "The first example is very simple. An Input control is declared and the resulting text is captured if " .. "the function returns true. By default, the function will return true on any text that is entered.") if Slab.Input('DrawInput_Basic', {Text = DrawInput_Basic}) then DrawInput_Basic = Slab.GetInputText() end Slab.NewLine() Slab.Textf( "The return behavior can be modified so that the function will only return true if the Enter/Return " .. "key is pressed. If the control loses focus without the Enter/Return key pressed, then the text will " .. "revert back to what it was before.") if Slab.Input('DrawInput_Basic_Return', {Text = DrawInput_Basic_Return, ReturnOnText = false}) then DrawInput_Basic_Return = Slab.GetInputText() end Slab.NewLine() Slab.Separator() Slab.Textf( "Input controls can be configured to only take numeric values. Input controls that are configured this way " .. "will allow the user to click and drag the control to alter the value by default. The user must double-click the ".. "control to manually enter a valid number.") if Slab.Input('DrawInput_Basic_Numbers', {Text = tostring(DrawInput_Basic_Numbers), NumbersOnly = true}) then DrawInput_Basic_Numbers = Slab.GetInputNumber() end Slab.NewLine() Slab.Textf( "These numeric controls can also have min and/or max values set. Below is an example where the " .. "numeric input control is clamped from 0.0 to 1.0. The drag step is also modified to be smaller for more precision.") Slab.Text("Min") Slab.SameLine() local DrawInput_Basic_Numbers_Clamped_Min_Options = { Text = tostring(DrawInput_Basic_Numbers_Clamped_Min), MaxNumber = DrawInput_Basic_Numbers_Clamped_Max, Step = DrawInput_Basic_Numbers_Clamped_Step, NumbersOnly = true, W = 50 } if Slab.Input('DrawInput_Basic_Numbers_Clamped_Min', DrawInput_Basic_Numbers_Clamped_Min_Options) then DrawInput_Basic_Numbers_Clamped_Min = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Max") Slab.SameLine() local DrawInput_Basic_Numbers_Clamped_Max_Options = { Text = tostring(DrawInput_Basic_Numbers_Clamped_Max), MinNumber = DrawInput_Basic_Numbers_Clamped_Min, Step = DrawInput_Basic_Numbers_Clamped_Step, NumbersOnly = true, W = 50 } if Slab.Input('DrawInput_Basic_Numbers_Clamped_Max', DrawInput_Basic_Numbers_Clamped_Max_Options) then DrawInput_Basic_Numbers_Clamped_Max = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Step") Slab.SameLine() local DrawInput_Basic_Numbers_Clamped_Step_Options = { Text = tostring(DrawInput_Basic_Numbers_Clamped_Step), MinNumber = 0, Step = 0.01, NumbersOnly = true, W = 50 } if Slab.Input('DrawInput_Basic_Numbers_Clamped_Step', DrawInput_Basic_Numbers_Clamped_Step_Options) then DrawInput_Basic_Numbers_Clamped_Step = Slab.GetInputNumber() end local DrawInput_Basic_Numbers_Clamped_Options = { Text = tostring(DrawInput_Basic_Numbers_Clamped), NumbersOnly = true, MinNumber = DrawInput_Basic_Numbers_Clamped_Min, MaxNumber = DrawInput_Basic_Numbers_Clamped_Max, Step = DrawInput_Basic_Numbers_Clamped_Step } if Slab.Input('DrawInput_Basic_Numbers_Clamped', DrawInput_Basic_Numbers_Clamped_Options) then DrawInput_Basic_Numbers_Clamped = Slab.GetInputNumber() end Slab.NewLine() Slab.Textf( "The click and drag functionality of numeric controls can also be disabled. This will make the input control behave like a " .. "standard text input control.") if Slab.Input('DrawInput_Basic_Numbers_NoDrag', {Text = tostring(DrawInput_Basic_Numbers_NoDrag), NumbersOnly = true, NoDrag = true}) then DrawInput_Basic_Numbers_NoDrag = Slab.GetInputNumber() end Slab.NewLine() Slab.Textf( "A slider can also be used for these numeric input controls. When configured this way, the value is altered based on where the " .. "user clicks and drags inside the control.") Slab.Text("Min") Slab.SameLine() if Slab.InputNumberDrag('DrawInput_Basic_Numbers_Slider_Min', DrawInput_Basic_Numbers_Slider_Min, nil, DrawInput_Basic_Numbers_Slider_Max, nil, {W = 50}) then DrawInput_Basic_Numbers_Slider_Min = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Max") Slab.SameLine() if Slab.InputNumberDrag('DrawInput_Basic_Numbers_Slider_Max', DrawInput_Basic_Numbers_Slider_Max, DrawInput_Basic_Numbers_Slider_Min, nil, nil, {W = 50}) then DrawInput_Basic_Numbers_Slider_Max = Slab.GetInputNumber() end if Slab.InputNumberSlider('DrawInput_Basic_Numbers_Slider', DrawInput_Basic_Numbers_Slider, DrawInput_Basic_Numbers_Slider_Min, DrawInput_Basic_Numbers_Slider_Max) then DrawInput_Basic_Numbers_Slider = Slab.GetInputNumber() end Slab.NewLine() Slab.Text("Sliders can also be drawn with a handle") if Slab.InputNumberSlider('DrawInput_Basic_Numbers_Slider_Handle', DrawInput_Basic_Numbers_Slider_Handle, DrawInput_Basic_Numbers_Slider_Min, DrawInput_Basic_Numbers_Slider_Max, {DrawSliderAsHandle = true}) then DrawInput_Basic_Numbers_Slider_Handle = Slab.GetInputNumber() end Slab.NewLine() Slab.Separator() Slab.Textf( "Input controls also allow for multi-line editing using the MultiLine option. The default text wrapping " .. "option is set to math.huge, but this can be modified with the MultiLineW option. The example below demonstrates " .. "how to set up a multi-line input control and shows how the size of the control can be modified.") Slab.NewLine() Slab.Text("MultiLineW") Slab.SameLine() if Slab.Input('DrawInput_MultiLine_Width', {Text = tostring(DrawInput_MultiLine_Width), NumbersOnly = true, ReturnOnText = false}) then DrawInput_MultiLine_Width = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Cursor Pos") Slab.SameLine() if Slab.Input('DrawInput_CursorPos', {Text = tostring(DrawInput_CursorPos), NumbersOnly = true, ReturnOnText = false, MinNumber = 0, W = 75}) then DrawInput_CursorPos = Slab.GetInputNumber() Slab.SetInputFocus('DrawInput_MultiLine') Slab.SetInputCursorPos(DrawInput_CursorPos) end Slab.SameLine() Slab.Text("Column") Slab.SameLine() if Slab.Input('DrawInput_CursorColumn', {Text = tostring(DrawInput_CursorColumn), NumbersOnly = true, ReturnOnText = false, MinNumber = 0, W = 75}) then DrawInput_CursorColumn = Slab.GetInputNumber() Slab.SetInputFocus('DrawInput_MultiLine') Slab.SetInputCursorPosLine(DrawInput_CursorColumn, DrawInput_CursorLine) end Slab.SameLine() Slab.Text("Line") Slab.SameLine() if Slab.Input('DrawInput_CursorLine', {Text = tostring(DrawInput_CursorLine), NumbersOnly = true, ReturnOnText = false, MinNumber = 0, W = 75}) then DrawInput_CursorLine = Slab.GetInputNumber() Slab.SetInputFocus('DrawInput_MultiLine') Slab.SetInputCursorPosLine(DrawInput_CursorColumn, DrawInput_CursorLine) end local W, H = Slab.GetWindowActiveSize() if Slab.Input('DrawInput_MultiLine', {Text = DrawInput_MultiLine, MultiLine = true, MultiLineW = DrawInput_MultiLine_Width, W = W, H = 150.0}) then DrawInput_MultiLine = Slab.GetInputText() end if Slab.IsInputFocused('DrawInput_MultiLine') then DrawInput_CursorPos, DrawInput_CursorColumn, DrawInput_CursorLine = Slab.GetInputCursorPos() end Slab.NewLine() Slab.Separator() Slab.Textf( "The input control also offers a way to highlight certain words with a custom color. Below is a list of keywords and the color used to define the word.") Slab.NewLine() local TextW, TextH = Slab.GetTextSize("") for K, V in pairs(DrawInput_Highlight_Table) do if Slab.Input('DrawInput_Highlight_Table_' .. K, {Text = K, ReturnOnText = false}) then DrawInput_Highlight_Table[K] = nil K = Slab.GetInputText() DrawInput_Highlight_Table[K] = V end Slab.SameLine({Pad = 20.0}) Slab.Rectangle({W = 50, H = TextH, Color = V}) if Slab.IsControlClicked() then DrawInput_Highlight_Table_Modify = K end Slab.SameLine({Pad = 20.0}) if Slab.Button("Delete", {H = TextH}) then DrawInput_Highlight_Table[K] = nil end end if Slab.Button("Add") then DrawInput_Highlight_Table['new'] = {1, 0, 0, 1} end if DrawInput_Highlight_Table_Modify ~= nil then local Result = Slab.ColorPicker({Color = DrawInput_Highlight_Table[DrawInput_Highlight_Table_Modify]}) if Result.Button ~= 0 then if Result.Button == 1 then DrawInput_Highlight_Table[DrawInput_Highlight_Table_Modify] = Result.Color end DrawInput_Highlight_Table_Modify = nil end end Slab.NewLine() if Slab.Input('DrawInput_Highlight', {Text = DrawInput_Highlight_Text, MultiLine = true, Highlight = DrawInput_Highlight_Table, W = W, H = 150.0}) then DrawInput_Highlight_Text = Slab.GetInputText() end end local DrawImage_Path = SLAB_FILE_PATH .. "/Internal/Resources/Textures/avatar.png" local DrawImage_Path_Icons = SLAB_FILE_PATH .. "/Internal/Resources/Textures/Icons.png" local DrawImage_Color = {1, 0, 0, 1} local DrawImage_Color_Edit = false local DrawImage_Scale = 1.0 local DrawImage_Scale_X = 1.0 local DrawImage_Scale_Y = 1.0 local DrawImage_Power = false local DrawImage_Power_Hovered = false local DrawImage_Power_On = {0, 1, 0, 1} local DrawImage_Power_Off = {1, 0, 0, 1} local DrawImage_Icon_X = 0 local DrawImage_Icon_Y = 0 local DrawImage_Icon_Move = false local DrawImage_UseOutline = true local DrawImage_OutlineWidth = 1 local DrawImage_OutlineColor = {0, 0, 0, 1} local DrawImage_OutlineColor_Edit = false local function DrawImage() Slab.Textf( "Images can be drawn within windows and react to user interaction. A path to an image can be specified through the options of " .. "the Image function. If this is done, Slab will manage the image resource and will use the path as a key to the resource.") Slab.Image('DrawImage_Basic', {Path = DrawImage_Path}) Slab.NewLine() Slab.Separator() Slab.Textf( "An image's color can be modified with the 'Color' option.") if Slab.Button("Change Color") then DrawImage_Color_Edit = true end if DrawImage_Color_Edit then local Result = Slab.ColorPicker({Color = DrawImage_Color}) if Result.Button ~= 0 then DrawImage_Color_Edit = false if Result.Button == 1 then DrawImage_Color = Result.Color end end end Slab.Image('DrawImage_Color', {Path = DrawImage_Path, Color = DrawImage_Color}) Slab.NewLine() Slab.Separator() Slab.Textf( "An outline can be applied to the image. The color and width of the outline is customizable.") Slab.Text("Use Outline") Slab.SameLine() if Slab.CheckBox(DrawImage_UseOutline) then DrawImage_UseOutline = not DrawImage_UseOutline end Slab.SameLine() Slab.Text("Width") Slab.SameLine() if Slab.Input('DrawImage_OutlineWidth', {Text = DrawImage_OutlineWidth, NumbersOnly = true, ReturnOnText = false, MinNumber = 1}) then DrawImage_OutlineWidth = Slab.GetInputNumber() end Slab.SameLine() if Slab.Button("Color") then DrawImage_OutlineColor_Edit = true end if DrawImage_OutlineColor_Edit then local Result = Slab.ColorPicker({Color = DrawImage_OutlineColor}) if Result.Button ~= 0 then DrawImage_OutlineColor_Edit = false if Result.Button == 1 then DrawImage_OutlineColor = Result.Color end end end Slab.Image('DrawImage_Outline', {Path = DrawImage_Path, UseOutline = DrawImage_UseOutline, OutlineW = DrawImage_OutlineWidth, OutlineColor = DrawImage_OutlineColor}) Slab.NewLine() Slab.Separator() Slab.Textf( "There is an option to modify the scale of an image. The scale can both be affected " .. "on the X or Y axis.") Slab.Text("Scale") Slab.SameLine() if Slab.Input('DrawImage_Scale', {Text = tostring(DrawImage_Scale), NumbersOnly = true, ReturnOnText = false, W = 75}) then DrawImage_Scale = Slab.GetInputNumber() DrawImage_Scale_X = DrawImage_Scale DrawImage_Scale_Y = DrawImage_Scale end Slab.SameLine({Pad = 6.0}) Slab.Text("Scale X") Slab.SameLine() if Slab.Input('DrawImage_Scale_X', {Text = tostring(DrawImage_Scale_X), NumbersOnly = true, ReturnOnText = false, W = 75}) then DrawImage_Scale_X = Slab.GetInputNumber() end Slab.SameLine({Pad = 6.0}) Slab.Text("Scale Y") Slab.SameLine() if Slab.Input('DrawImage_Scale_Y', {Text = tostring(DrawImage_Scale_Y), NumbersOnly = true, ReturnOnText = false, W = 75}) then DrawImage_Scale_Y = Slab.GetInputNumber() end Slab.Image('DrawImage_Scale', {Path = DrawImage_Path, ScaleX = DrawImage_Scale_X, ScaleY = DrawImage_Scale_Y}) Slab.NewLine() Slab.Separator() Slab.Textf( "Images can also have interactions through the control API. The left image will change when the mouse is hovered " .. "while the right image will change on click.") Slab.Image('DrawImage_Hover', {Path = DrawImage_Path, Color = DrawImage_Power_Hovered and DrawImage_Power_On or DrawImage_Power_Off}) DrawImage_Power_Hovered = Slab.IsControlHovered() Slab.SameLine({Pad = 12.0}) Slab.Image('DrawImage_Click', {Path = DrawImage_Path, Color = DrawImage_Power and DrawImage_Power_On or DrawImage_Power_Off}) if Slab.IsControlClicked() then DrawImage_Power = not DrawImage_Power end Slab.NewLine() Slab.Separator() Slab.Textf( "A sub region can be defined to draw a section of an image. Move the rectangle around and observe the image on the right.") local X, Y = Slab.GetCursorPos() local AbsX, AbsY = Slab.GetCursorPos({Absolute = true}) Slab.Image('DrawImage_Icons', {Path = DrawImage_Path_Icons}) if Slab.IsControlClicked() then local MouseX, MouseY = Slab.GetMousePositionWindow() local Left = AbsX + DrawImage_Icon_X local Right = Left + 50.0 local Top = AbsY + DrawImage_Icon_Y local Bottom = Top + 50.0 if Left <= MouseX and MouseX <= Right and Top <= MouseY and MouseY <= Bottom then DrawImage_Icon_Move = true end end if Slab.IsMouseReleased() then DrawImage_Icon_Move = false end local W, H = Slab.GetControlSize() if DrawImage_Icon_Move then local DeltaX, DeltaY = Slab.GetMouseDelta() DrawImage_Icon_X = math.max(DrawImage_Icon_X + DeltaX, 0.0) DrawImage_Icon_X = math.min(DrawImage_Icon_X, W - 50.0) DrawImage_Icon_Y = math.max(DrawImage_Icon_Y + DeltaY, 0.0) DrawImage_Icon_Y = math.min(DrawImage_Icon_Y, H - 50.0) end Slab.SetCursorPos(X + DrawImage_Icon_X, Y + DrawImage_Icon_Y) Slab.Rectangle({Mode = 'line', Color = {0, 0, 0, 1}, W = 50.0, H = 50.0}) Slab.SetCursorPos(X + W + 12.0, Y) Slab.Image('DrawImage_Icons_Region', { Path = DrawImage_Path_Icons, SubX = DrawImage_Icon_X, SubY = DrawImage_Icon_Y, SubW = 50.0, SubH = 50.0 }) end local DrawCursor_NewLines = 1 local DrawCursor_SameLinePad = 4.0 local DrawCursor_X = nil local DrawCursor_Y = nil local DrawCursor_Indent = 14 local function DrawCursor() Slab.Textf( "Slab offers a way to manage the drawing of controls through the cursor. Whenever a control is used, the cursor is ".. "automatically advanced based on the size of the control. By default, cursors are advanced vertically downward based " .. "on the control's height. However, functions are provided to move the cursor back up to the previous line or create " .. "an empty line to advance the cursor downward.") for I = 1, DrawCursor_NewLines, 1 do Slab.NewLine() end Slab.Textf( "There is a new line between this text and the above description. Modify the number of new lines using the " .. "input box below.") if Slab.Input('DrawCursor_NewLines', {Text = tostring(DrawCursor_NewLines), NumbersOnly = true, ReturnOnText = false, MinNumber = 0}) then DrawCursor_NewLines = Slab.GetInputNumber() end Slab.NewLine() Slab.Separator() Slab.Textf( "Using the SameLine function, controls can be layed out on a single line with additional padding. Below are two buttons on " .. "the same line with some padding. Use the input field below to modify the padding.") Slab.Button("One") Slab.SameLine({Pad = DrawCursor_SameLinePad}) Slab.Button("Two") if Slab.Input('DrawCursor_SameLinePad', {Text = tostring(DrawCursor_SameLinePad), NumbersOnly = true, ReturnOnText = false}) then DrawCursor_SameLinePad = Slab.GetInputNumber() end Slab.NewLine() Slab.Textf( "The SameLine function can also vertically center the next item based on the previous control. This is useful for labeling " .. "items that are much bigger than the text such as images.") Slab.Image('DrawCursor_Image', {Path = DrawImage_Path}) Slab.SameLine({CenterY = true}) Slab.Text("This text is centered with respect to the previous image.") Slab.NewLine() Slab.Separator() Slab.Textf( "Slab offers functions to retrieve and set the cursor position. The GetCursorPos function will return the cursor position " .. "relative to the current window. An option can be passed to retrieve the absolute position of the cursor with respect " .. "to the viewport.") local X, Y = Slab.GetCursorPos() Slab.Text("Cursor X: " .. X) Slab.SameLine() Slab.Text("Cursor Y: " .. Y) local AbsX, AbsY = Slab.GetCursorPos({Absolute = true}) Slab.Text("Absolute X: " .. AbsX) Slab.SameLine() Slab.Text("Absolute Y: " .. AbsY) if DrawCursor_X == nil then DrawCursor_X, DrawCursor_Y = Slab.GetCursorPos() end if Slab.Input('DrawCursor_X', {Text = tostring(DrawCursor_X), NumbersOnly = true, ReturnOnText = false}) then DrawCursor_X = Slab.GetInputNumber() end Slab.SameLine() if Slab.Input('DrawCursor_Y', {Text = tostring(DrawCursor_Y), NumbersOnly = true, ReturnOnText = false}) then DrawCursor_Y = Slab.GetInputNumber() end Slab.SetCursorPos(DrawCursor_X, DrawCursor_Y + 30.0) Slab.Text("Use the input fields to move this text.") Slab.NewLine() Slab.Separator() Slab.Textf( "There are also API functions to indent or unindent the anchored X position of the cursor. The function takes in a " .. "number which represents how far to advance/retreat in pixels from the current anchored position. If no number is " .. "given, then the default value is used which is defined by the Indent property located in the Style table. Below " .. "are examples of how the Indent/Unindent functions can be used and while the example mainly uses Text controls, these " .. "functions can be applied to any controls.") Slab.NewLine() Slab.Text("Line 1") Slab.Text("Line 2") Slab.Indent() Slab.Text("Indented Line 1") Slab.Text("Indented Line 2") Slab.Indent() Slab.Text("Indented Line 3") Slab.Unindent() Slab.Text("Unindented Line 1") Slab.Text("Unindented Line 2") Slab.Unindent() Slab.Text("Unindented Line 3") Slab.NewLine() Slab.Indent(DrawCursor_Indent) Slab.Text("Indent:") Slab.SameLine() if Slab.Input('DrawCursor_Indent', {Text = tostring(DrawCursor_Indent), NumbersOnly = true, ReturnOnText = false}) then DrawCursor_Indent = Slab.GetInputNumber() end end local DrawListBox_Basic_Selected = 1 local DrawListBox_Basic_Count = 10 local DrawListBox_Advanced_Selected = 1 local function DrawListBox() Slab.Textf( "A list box is a scrollable region that contains a list of elements that a user can interact with. The API is flexible " .. "so that each element in the list can be rendered in any way desired. Below are a few examples on different ways a list " .. "box can be used.") Slab.NewLine() local Clear = false Slab.Text("Count") Slab.SameLine() if Slab.Input('DrawListBox_Basic_Count', {Text = tostring(DrawListBox_Basic_Count), NumbersOnly = true, MinNumber = 0, ReturnOnText = false}) then DrawListBox_Basic_Count = Slab.GetInputNumber() Clear = true end Slab.NewLine() Slab.BeginListBox('DrawListBox_Basic', {Clear = Clear}) for I = 1, DrawListBox_Basic_Count, 1 do Slab.BeginListBoxItem('DrawListBox_Basic_Item_' .. I, {Selected = I == DrawListBox_Basic_Selected}) Slab.Text("List Box Item " .. I) if Slab.IsListBoxItemClicked() then DrawListBox_Basic_Selected = I end Slab.EndListBoxItem() end Slab.EndListBox() Slab.NewLine() Slab.Separator() Slab.Textf( "Each list box can contain more than just text. Below is an example of list items with a triangle and a label.") Slab.NewLine() Slab.BeginListBox('DrawListBox_Advanced') local Rotation = 0 for I = 1, 4, 1 do Slab.BeginListBoxItem('DrawListBox_Advanced_Item_' .. I, {Selected = I == DrawListBox_Advanced_Selected}) Slab.Triangle({Radius = 24.0, Rotation = Rotation}) Slab.SameLine({CenterY = true}) Slab.Text("Triangle " .. I) if Slab.IsListBoxItemClicked() then DrawListBox_Advanced_Selected = I end Slab.EndListBoxItem() Rotation = Rotation + 90 end Slab.EndListBox() end local DrawTree_Icon_Path = SLAB_FILE_PATH .. "/Internal/Resources/Textures/Folder.png" local DrawTree_Opened_Selected = 1 local DrawTree_Tables = nil local function DrawTree() Slab.Textf( "Trees allow data to be viewed in a hierarchy. Trees can also contain leaf nodes which have no children.") Slab.NewLine() if Slab.BeginTree('DrawTree_Root', {Label = "Root"}) then if Slab.BeginTree('DrawTree_Child_1', {Label = "Child 1"}) then Slab.BeginTree('DrawTree_Child_1_Leaf_1', {Label = "Leaf 1", IsLeaf = true}) Slab.EndTree() end Slab.BeginTree('DrawTree_Leaf_1', {Label = "Leaf 2", IsLeaf = true}) if Slab.BeginTree('DrawTree_Child_2', {Label = "Child 2"}) then Slab.BeginTree('DrawTree_Child_2_Leaf_3', {Label = "Leaf 3", IsLeaf = true}) Slab.EndTree() end Slab.EndTree() end Slab.NewLine() Slab.Separator() Slab.Textf( "The hot zone of a tree item starts at the expander and extends to the width of the window's content. " .. "This can be configured to only allow the tree item to be opened/closed with the expander.") Slab.NewLine() if Slab.BeginTree('DrawTree_Root_NoHighlight', {Label = "Root", OpenWithHighlight = false}) then Slab.BeginTree('DrawTree_Leaf', {Label = "Leaf", IsLeaf = true}) if Slab.BeginContextMenuItem() then Slab.MenuItem("Leaf Option 1") Slab.MenuItem("Leaf Option 2") Slab.EndContextMenu() end Slab.EndTree() end Slab.NewLine() Slab.Separator() Slab.Textf( "Tree items can have an icon associated with them. A loaded Image object or path to an image can be " .. "specified.") Slab.NewLine() local Icon = {Path = DrawImage_Path_Icons, SubX = 0.0, SubY = 0.0, SubW = 50.0, SubH = 50.0} if Slab.BeginTree('DrawTree_Root_Icon', {Label = "Folder", Icon = Icon}) then Slab.BeginTree('DrawTree_Item_1', {Label = "Item 1", IsLeaf = true}) Slab.BeginTree('DrawTree_Item_2', {Label = "Item 2", IsLeaf = true}) if Slab.BeginTree('DrawTree_Child_1', {Label = "Folder", Icon = Icon}) then Slab.BeginTree('DrawTree_Item_3', {Label = "Item 3", IsLeaf = true}) Slab.BeginTree('DrawTree_Item_4', {Label = "Item 4", IsLeaf = true}) Slab.EndTree() end Slab.EndTree() end Slab.NewLine() Slab.Separator() Slab.Textf( "A tree item can be specified to be forced open with the IsOpen option as shown in the example below. The example " .. "also shows how tree items can have the selection rectangle permanently rendered.") Slab.NewLine() if Slab.BeginTree('DrawTree_Root_Opened', {Label = "Root", IsOpen = true}) then for I = 1, 5, 1 do Slab.BeginTree('DrawTree_Item_' .. I, {Label = "Item " .. I, IsLeaf = true, IsSelected = I == DrawTree_Opened_Selected}) if Slab.IsControlClicked() then DrawTree_Opened_Selected = I end end Slab.EndTree() end Slab.NewLine() Slab.Separator() Slab.Textf( "Tree Ids can also be specified as a table. This allows the user to use a transient table to identify a particular tree " .. "element. The tree system has been updated so that any Ids that are used as tables will have the key be removed when the " .. "referenced table is garbage collected. This gives the user the ability to create thousands of tree elements and have " .. "the tree system keep the number of persistent elements to a minimum. The default label used for these elements will be " .. "the memory location of the table, so it is highly recommended to set the 'Label' option for the table. These table " .. "elements will also be forced to disable saving settings to disk as the referenced key is a table and is transient. " .. "As of version 0.7, this feature is only available for tree controls.") Slab.NewLine() Slab.Textf( "The example below shows 5 tables that have been instanced and have an associated tree element. The right-click context " .. "menu for the root allows for additions to this list. The right-click context menu for each item contains the option " .. "to remove the individual element from the list and have that table garbage collected. This removal will also remove the " .. "associated tree element.") Slab.NewLine() if DrawTree_Tables == nil then DrawTree_Tables = {} for I = 1, 5, 1 do table.insert(DrawTree_Tables, {}) end end local RemoveIndex = -1 if Slab.BeginTree('Root', {IsOpen = true}) then if Slab.BeginContextMenuItem() then if Slab.MenuItem("Add") then table.insert(DrawTree_Tables, {}) end Slab.EndContextMenu() end for I, V in ipairs(DrawTree_Tables) do Slab.BeginTree(V, {IsLeaf = true}) if Slab.BeginContextMenuItem() then if Slab.MenuItem("Remove") then RemoveIndex = I end Slab.EndContextMenu() end end Slab.EndTree() end if RemoveIndex > 0 then table.remove(DrawTree_Tables, RemoveIndex) end end local DrawDialog_MessageBox = false local DrawDialog_MessageBox_Title = "Message Box" local DrawDialog_MessageBox_Message = "This is a message." local DrawDialog_FileDialog = '' local DrawDialog_FileDialog_Result = "" local function DrawDialog() Slab.Textf( "Dialog boxes are windows that rendered on top of everything else. These windows will consume input from all other windows " .. "and controls. These are useful for forcing users to interact with a window of importance, such as message boxes and " .. "file dialogs.") Slab.NewLine() Slab.Textf( "By clicking the button below, an example of a simple dialog box will be rendered.") if Slab.Button("Open Basic Dialog") then Slab.OpenDialog('DrawDialog_Basic') end if Slab.BeginDialog('DrawDialog_Basic', {Title = "Basic Dialog"}) then Slab.Text("This is a basic dialog box.") if Slab.Button("Close") then Slab.CloseDialog() end Slab.EndDialog() end Slab.NewLine() Slab.Separator() Slab.Textf( "Slab offers support for common dialog boxes such as message boxes. To display a message box, Slab.MessageBox must be called every " .. "frame. The buttons to be drawn must be passed in through the Buttons option. Once the user has made a selection, the button that was " .. "clicked is returned and the program can handle the response accordingly.") Slab.NewLine() Slab.Text("Title") Slab.SameLine() if Slab.Input('DrawDialog_MessageBox_Title', {Text = DrawDialog_MessageBox_Title}) then DrawDialog_MessageBox_Title = Slab.GetInputText() end Slab.NewLine() Slab.Text("Message") if Slab.Input('DrawDialog_MessageBox_Message', {Text = DrawDialog_MessageBox_Message, MultiLine = true, H = 75}) then DrawDialog_MessageBox_Message = Slab.GetInputText() end Slab.NewLine() if Slab.Button("Show Message Box") then DrawDialog_MessageBox = true end if DrawDialog_MessageBox then local Result = Slab.MessageBox(DrawDialog_MessageBox_Title, DrawDialog_MessageBox_Message, {Buttons = {"OK"}}) if Result ~= "" then DrawDialog_MessageBox = false end end Slab.NewLine() Slab.Separator() Slab.Textf( "Slab offers a file dialog box so that user can select to open or save a file. This behaves similar to file dialogs found on " .. "various operating systems. Files can be filtered and a starting directory can be set. There are options for the user to select " .. "a single item or multiple items. As with the message box, the FileDialog option must be called every frame and the user response " .. "must be handled by the program.") Slab.NewLine() if Slab.Button("Open File") then DrawDialog_FileDialog = 'openfile' end Slab.SameLine() if Slab.Button("Open Directory") then DrawDialog_FileDialog = 'opendirectory' end Slab.SameLine() if Slab.Button("Save File") then DrawDialog_FileDialog = 'savefile' end if DrawDialog_FileDialog ~= '' then local Result = Slab.FileDialog({AllowMultiSelect = false, Type = DrawDialog_FileDialog}) if Result.Button ~= "" then DrawDialog_FileDialog = '' if Result.Button == "OK" then DrawDialog_FileDialog_Result = Result.Files[1] end end end Slab.Textf( "Selected file: " .. DrawDialog_FileDialog_Result) end local DrawInteraction_MouseClicked_Left = 0 local DrawInteraction_MouseClicked_Right = 0 local DrawInteraction_MouseClicked_Middle = 0 local DrawInteraction_MouseReleased_Left = 0 local DrawInteraction_MouseReleased_Right = 0 local DrawInteraction_MouseReleased_Middle = 0 local DrawInteraction_MouseDoubleClicked_Left = 0 local DrawInteraction_MouseDoubleClicked_Right = 0 local DrawInteraction_MouseDoubleClicked_Middle = 0 local DrawInteraction_MouseVoidClicked_Left = 0 local DrawInteraction_MouseCustomCursors = nil local DrawInteraction_KeyPressed_A = 0 local DrawInteraction_KeyPressed_S = 0 local DrawInteraction_KeyPressed_D = 0 local DrawInteraction_KeyPressed_F = 0 local DrawInteraction_KeyReleased_A = 0 local DrawInteraction_KeyReleased_S = 0 local DrawInteraction_KeyReleased_D = 0 local DrawInteraction_KeyReleased_F = 0 local function DrawInteraction() Slab.Textf( "Slab offers functions to query the user's input on a given frame. There are also functions to query for input on the most " .. "recently declared control. This can allow the implementation to use custom logic for controls to create custom behaviors.") Slab.NewLine() Slab.Textf( "Below are functions that query the state of the mouse. The IsMouseDown checks to see if a specific button is down on that " .. "frame. The IsMouseClicked will check to see if the state of a button went from up to down on that frame and the IsMouseReleased " .. "function checks to see if a button went from down to up on that frame.") local Left = Slab.IsMouseDown(1) local Right = Slab.IsMouseDown(2) local Middle = Slab.IsMouseDown(3) Slab.NewLine() Slab.Text("Left") Slab.SameLine() Slab.Text(Left and "Down" or "Up") Slab.Text("Right") Slab.SameLine() Slab.Text(Right and "Down" or "Up") Slab.Text("Middle") Slab.SameLine() Slab.Text(Middle and "Down" or "Up") Slab.NewLine() if Slab.IsMouseClicked(1) then DrawInteraction_MouseClicked_Left = DrawInteraction_MouseClicked_Left + 1 end if Slab.IsMouseClicked(2) then DrawInteraction_MouseClicked_Right = DrawInteraction_MouseClicked_Right + 1 end if Slab.IsMouseClicked(3) then DrawInteraction_MouseClicked_Middle = DrawInteraction_MouseClicked_Middle + 1 end if Slab.IsMouseReleased(1) then DrawInteraction_MouseReleased_Left = DrawInteraction_MouseReleased_Left + 1 end if Slab.IsMouseReleased(2) then DrawInteraction_MouseReleased_Right = DrawInteraction_MouseReleased_Right + 1 end if Slab.IsMouseReleased(3) then DrawInteraction_MouseReleased_Middle = DrawInteraction_MouseReleased_Middle + 1 end Slab.Text("Left Clicked: " .. DrawInteraction_MouseClicked_Left) Slab.SameLine() Slab.Text("Released: " .. DrawInteraction_MouseReleased_Left) Slab.Text("Right Clicked: " .. DrawInteraction_MouseClicked_Right) Slab.SameLine() Slab.Text("Released: " .. DrawInteraction_MouseReleased_Right) Slab.Text("Middle Clicked: " .. DrawInteraction_MouseClicked_Middle) Slab.SameLine() Slab.Text("Released: " .. DrawInteraction_MouseReleased_Middle) Slab.NewLine() Slab.Textf( "Slab offers functions to detect if the mouse was double-clicked or if a mouse button is being dragged.") Slab.NewLine() if Slab.IsMouseDoubleClicked(1) then DrawInteraction_MouseDoubleClicked_Left = DrawInteraction_MouseDoubleClicked_Left + 1 end if Slab.IsMouseDoubleClicked(2) then DrawInteraction_MouseDoubleClicked_Right = DrawInteraction_MouseDoubleClicked_Right + 1 end if Slab.IsMouseDoubleClicked(3) then DrawInteraction_MouseDoubleClicked_Middle = DrawInteraction_MouseDoubleClicked_Middle + 1 end Slab.Text("Left Double Clicked: " .. DrawInteraction_MouseDoubleClicked_Left) Slab.Text("Right Double Clicked: " .. DrawInteraction_MouseDoubleClicked_Right) Slab.Text("Middle Double Clicked: " .. DrawInteraction_MouseDoubleClicked_Middle) Slab.NewLine() local LeftDrag = Slab.IsMouseDragging(1) local RightDrag = Slab.IsMouseDragging(2) local MiddleDrag = Slab.IsMouseDragging(3) Slab.Text("Left Drag: " .. tostring(LeftDrag)) Slab.Text("Right Drag: " .. tostring(RightDrag)) Slab.Text("Middle Drag: " .. tostring(MiddleDrag)) Slab.NewLine() Slab.Textf( "The mouse position relative to the viewport and relative to the current window can also be queried. Slab also offers retrieving " .. "the mouse delta.") Slab.NewLine() local X, Y = Slab.GetMousePosition() local WinX, WinY = Slab.GetMousePositionWindow() local DeltaX, DeltaY = Slab.GetMouseDelta() Slab.Text("X: " .. X .. " Y: " .. Y) Slab.Text("Window X: " .. WinX .. " Window Y: " .. WinY) Slab.Text("Delta X: " .. DeltaX .. " Delta Y: " .. DeltaY) Slab.Textf( "Slab also offers functions to test if the user is interacting with the non-UI layer. The IsVoidHovered and IsVoidClicked " .. "behave the same way as IsControlHovered and IsControlClicked except will only return true when it is in a non-UI area.") Slab.NewLine() if Slab.IsVoidClicked(1) then DrawInteraction_MouseVoidClicked_Left = DrawInteraction_MouseVoidClicked_Left + 1 end local IsVoidHovered = Slab.IsVoidHovered() Slab.Text("Left Void Clicked: " .. DrawInteraction_MouseVoidClicked_Left) Slab.Text("Is Void Hovered: " .. tostring(IsVoidHovered)) Slab.NewLine() Slab.Textf( "The rendered mouse can also be customized. This is done by overriding what the default system cursor displays. A custom Image " .. "can be supplied but must be managed by the developer. Alternatively, 'nil' can be passed to disable rendering any cursor for a " .. "given system cursor type. Below is a list of available system cursors that can be overridden. Each custom cursor is associated with " .. "a test image for this example.") if DrawInteraction_MouseCustomCursors == nil then DrawInteraction_MouseCustomCursors = {} local Image = love.graphics.newImage(DrawImage_Path_Icons) local Corner = love.graphics.newQuad(150, 0, 50, 50, Image:getWidth(), Image:getHeight()) local Cursor = love.graphics.newQuad(200, 0, 50, 50, Image:getWidth(), Image:getHeight()) local WestEast = love.graphics.newQuad(50, 50, 50, 50, Image:getWidth(), Image:getHeight()) local NorthSouth = love.graphics.newQuad(100, 50, 50, 50, Image:getWidth(), Image:getHeight()) local Hand = love.graphics.newQuad(0, 50, 50, 50, Image:getWidth(), Image:getHeight()) local IBeam = love.graphics.newQuad(150, 50, 50, 50, Image:getWidth(), Image:getHeight()) DrawInteraction_MouseCustomCursors['arrow'] = {Image = Image, Quad = Cursor} DrawInteraction_MouseCustomCursors['sizewe'] = {Image = Image, Quad = WestEast} DrawInteraction_MouseCustomCursors['sizens'] = {Image = Image, Quad = NorthSouth} DrawInteraction_MouseCustomCursors['sizenesw'] = {Image = Image, Quad = Corner} DrawInteraction_MouseCustomCursors['sizenwse'] = {Image = Image, Quad = Corner} DrawInteraction_MouseCustomCursors['ibeam'] = {Image = Image, Quad = IBeam} DrawInteraction_MouseCustomCursors['hand'] = {Image = Image, Quad = Hand} end Slab.NewLine() for K, V in pairs(DrawInteraction_MouseCustomCursors) do if Slab.CheckBox(V.Enabled, K) then V.Enabled = not V.Enabled if V.Enabled then Slab.SetCustomMouseCursor(K, V.Image, V.Quad) else Slab.ClearCustomMouseCursor(K) end end end Slab.NewLine() Slab.Separator() Slab.Textf( "Slab offers functions to check for the state of a specific keyboard key. The key code to use are the ones defined by LÖVE " .. "which can be found on the wiki. Below we will check for the key states of the A, S, D, F keys.") Slab.NewLine() local IsDown_A = Slab.IsKeyDown('a') local IsDown_S = Slab.IsKeyDown('s') local IsDown_D = Slab.IsKeyDown('d') local IsDown_F = Slab.IsKeyDown('f') if Slab.IsKeyPressed('a') then DrawInteraction_KeyPressed_A = DrawInteraction_KeyPressed_A + 1 end if Slab.IsKeyPressed('s') then DrawInteraction_KeyPressed_S = DrawInteraction_KeyPressed_S + 1 end if Slab.IsKeyPressed('d') then DrawInteraction_KeyPressed_D = DrawInteraction_KeyPressed_D + 1 end if Slab.IsKeyPressed('f') then DrawInteraction_KeyPressed_F = DrawInteraction_KeyPressed_F + 1 end if Slab.IsKeyReleased('a') then DrawInteraction_KeyReleased_A = DrawInteraction_KeyReleased_A + 1 end if Slab.IsKeyReleased('s') then DrawInteraction_KeyReleased_S = DrawInteraction_KeyReleased_S + 1 end if Slab.IsKeyReleased('d') then DrawInteraction_KeyReleased_D = DrawInteraction_KeyReleased_D + 1 end if Slab.IsKeyReleased('f') then DrawInteraction_KeyReleased_F = DrawInteraction_KeyReleased_F + 1 end Slab.Text("A Down: " .. tostring(IsDown_A)) Slab.Text("S Down: " .. tostring(IsDown_S)) Slab.Text("D Down: " .. tostring(IsDown_D)) Slab.Text("F Down: " .. tostring(IsDown_F)) Slab.NewLine() Slab.Text("A Pressed: " .. DrawInteraction_KeyPressed_A) Slab.Text("S Pressed: " .. DrawInteraction_KeyPressed_S) Slab.Text("D Pressed: " .. DrawInteraction_KeyPressed_D) Slab.Text("F Pressed: " .. DrawInteraction_KeyPressed_F) Slab.NewLine() Slab.Text("A Released: " .. DrawInteraction_KeyReleased_A) Slab.Text("S Released: " .. DrawInteraction_KeyReleased_S) Slab.Text("D Released: " .. DrawInteraction_KeyReleased_D) Slab.Text("F Released: " .. DrawInteraction_KeyReleased_F) end local DrawShapes_Rectangle_Color = {1, 0, 0, 1} local DrawShapes_Rectangle_ChangeColor = false local DrawShapes_Rectangle_Rounding = {0, 0, 2.0, 2.0} local DrawShapes_Circle_Radius = 32.0 local DrawShapes_Circle_Segments = 24 local DrawShapes_Circle_Mode = 'fill' local DrawShapes_Triangle_Radius = 32.0 local DrawShapes_Triangle_Rotation = 0 local DrawShapes_Triangle_Mode = 'fill' local DrawShapes_Modes = {'fill', 'line'} local DrawShapes_Line_Width = 1.0 local DrawShapes_Curve = {0, 0, 150, 150, 300, 0} local DrawShapes_ControlPoint_Size = 7.5 local DrawShapes_ControlPoint_Index = 0 local DrawShapes_Polygon = {10, 10, 150, 25, 175, 75, 50, 125} local DrawShapes_Polygon_Mode = 'fill' local function DrawShapes_Rectangle_Rounding_Input(Corner, Index) Slab.Text(Corner) Slab.SameLine() if Slab.Input('DrawShapes_Rectangle_Rounding_' .. Corner, {Text = tostring(DrawShapes_Rectangle_Rounding[Index]), NumbersOnly = true, MinNumber = 0, ReturnOnText = false}) then DrawShapes_Rectangle_Rounding[Index] = Slab.GetInputNumber() end end local function DrawShapes() Slab.Textf( "Slab offers functions to draw basic shapes to the window. These shapes can complement the controls provided by Slab.") Slab.NewLine() Slab.Textf( "Below is an invisible button combined with a rectangle. Click on the rectangle to change the color.") local X, Y = Slab.GetCursorPos() Slab.Rectangle({W = 150, H = 25, Color = DrawShapes_Rectangle_Color}) Slab.SetCursorPos(X, Y) if Slab.Button("", {W = 150, H = 25, Invisible = true}) then DrawShapes_Rectangle_ChangeColor = true end if DrawShapes_Rectangle_ChangeColor then local Result = Slab.ColorPicker({Color = DrawShapes_Rectangle_Color}) if Result.Button ~= 0 then DrawShapes_Rectangle_ChangeColor = false if Result.Button == 1 then DrawShapes_Rectangle_Color = Result.Color end end end Slab.NewLine() Slab.Textf( "Rectangle corner rounding can be defined in multiple ways. The rounding option can take a single number, which will apply rounding to all corners. The option " .. "can also accept a table, with each index affecting a single corner. The order this happens in is top left, top right, bottom right, and bottom left.") Slab.NewLine() DrawShapes_Rectangle_Rounding_Input('TL', 1) Slab.SameLine() DrawShapes_Rectangle_Rounding_Input('TR', 2) Slab.SameLine() DrawShapes_Rectangle_Rounding_Input('BR', 3) Slab.SameLine() DrawShapes_Rectangle_Rounding_Input('BL', 4) Slab.NewLine() Slab.Rectangle({W = 150.0, H = 75.0, Rounding = DrawShapes_Rectangle_Rounding, Outline = true, Color = {0, 1, 0, 1}}) Slab.NewLine() Slab.Separator() Slab.Textf( "Circles are drawn by defining a radius. Along with the color the number of segments can be set as well.") Slab.NewLine() Slab.Text("Radius") Slab.SameLine() if Slab.Input('DrawShapes_Circle_Radius', {Text = tostring(DrawShapes_Circle_Radius), NumbersOnly = true, MinNumber = 0, ReturnOnText = false}) then DrawShapes_Circle_Radius = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Segments") Slab.SameLine() if Slab.Input('DrawShapes_Circle_Segments', {Text = tostring(DrawShapes_Circle_Segments), NumbersOnly = true, MinNumber = 0, ReturnOnText = false}) then DrawShapes_Circle_Segments = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Mode") Slab.SameLine() if Slab.BeginComboBox('DrawShapes_Circle_Mode', {Selected = DrawShapes_Circle_Mode}) then for I, V in ipairs(DrawShapes_Modes) do if Slab.TextSelectable(V) then DrawShapes_Circle_Mode = V end end Slab.EndComboBox() end Slab.Circle({Radius = DrawShapes_Circle_Radius, Segments = DrawShapes_Circle_Segments, Color = {1, 1, 1, 1}, Mode = DrawShapes_Circle_Mode}) Slab.NewLine() Slab.Separator() Slab.Textf( "Triangles are drawn by defining a radius, which is the length from the center of the triangle to the 3 points. A rotation in degrees " .. "can be specified to rotate the triangle.") Slab.NewLine() Slab.Text("Radius") Slab.SameLine() if Slab.Input('DrawShapes_Triangle_Radius', {Text = tostring(DrawShapes_Triangle_Radius), NumbersOnly = true, MinNumber = 0, ReturnOnText = false}) then DrawShapes_Triangle_Radius = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Rotation") Slab.SameLine() if Slab.Input('DrawShapes_Triangle_Rotation', {Text = tostring(DrawShapes_Triangle_Rotation), NumbersOnly = true, MinNumber = 0, ReturnOnText = false}) then DrawShapes_Triangle_Rotation = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Mode") Slab.SameLine() if Slab.BeginComboBox('DrawShapes_Triangle_Mode', {Selected = DrawShapes_Triangle_Mode}) then for I, V in ipairs(DrawShapes_Modes) do if Slab.TextSelectable(V) then DrawShapes_Triangle_Mode = V end end Slab.EndComboBox() end Slab.Triangle({Radius = DrawShapes_Triangle_Radius, Rotation = DrawShapes_Triangle_Rotation, Color = {0, 1, 0, 1}, Mode = DrawShapes_Triangle_Mode}) Slab.NewLine() Slab.Separator() Slab.Textf( "Lines are defined by two points. The function only takes in a single point which defines the end point while the start point is defined by the current " .. "cursor position. Both the line width and color can be defined.") Slab.NewLine() Slab.Text("Width") Slab.SameLine() if Slab.Input('DrawShapes_Line_Width', {Text = tostring(DrawShapes_Line_Width), NumbersOnly = true, ReturnOnText = false, MinNumber = 1.0}) then DrawShapes_Line_Width = Slab.GetInputNumber() end Slab.NewLine() X, Y = Slab.GetCursorPos({Absolute = true}) local WinW, WinH = Slab.GetWindowActiveSize() Slab.Line(X + WinW * 0.5, Y, {Width = DrawShapes_Line_Width, Color = {1, 1, 0, 1}}) Slab.NewLine() Slab.Separator() Slab.Textf( "Bezier curves can be defined through a set of points and added to a Slab window. The points given must be in local space. Slab will translate the " .. "curve to the current cursor position. Along with the ability to draw the curve, Slab offers functions to query information about the curve, such as " .. "the number of control points defined, the position of a control point, and the ability to evaluate the position of a curve given a Time value. " .. "There is also a function to evaluate the curve with the current X mouse position.") Slab.NewLine() Slab.Curve(DrawShapes_Curve) X, Y = Slab.GetCursorPos({Absolute = true}) Slab.SameLine({CenterY = true, Pad = 16}) local EvalX, EvalY = Slab.EvaluateCurveMouse() Slab.Text(string.format("X: %.2f Y: %.2f", EvalX, EvalY)) EvalX, EvalY = Slab.EvaluateCurveMouse({LocalSpace = false}) Slab.SetCursorPos(EvalX, EvalY, {Absolute = true}) Slab.Circle({Color = {1, 1, 1, 1}, Radius = DrawShapes_ControlPoint_Size * 0.5}) local HalfSize = DrawShapes_ControlPoint_Size * 0.5 for I = 1, Slab.GetCurveControlPointCount(), 1 do local PX, PY = Slab.GetCurveControlPoint(I, {LocalSpace = false}) Slab.SetCursorPos(PX - HalfSize, PY - HalfSize, {Absolute = true}) Slab.Rectangle({W = DrawShapes_ControlPoint_Size, H = DrawShapes_ControlPoint_Size, Color = {1, 1, 1, 1}}) if Slab.IsControlClicked() then DrawShapes_ControlPoint_Index = I end end if DrawShapes_ControlPoint_Index > 0 and Slab.IsMouseDragging() then local DeltaX, DeltaY = Slab.GetMouseDelta() local P2 = DrawShapes_ControlPoint_Index * 2 local P1 = P2 - 1 DrawShapes_Curve[P1] = DrawShapes_Curve[P1] + DeltaX DrawShapes_Curve[P2] = DrawShapes_Curve[P2] + DeltaY end if Slab.IsMouseReleased() then DrawShapes_ControlPoint_Index = 0 end Slab.SetCursorPos(X, Y, {Absolute = true}) Slab.NewLine() Slab.Separator() Slab.Textf( "Polygons can be drawn by passing in a list of points into the Polygon function. The points, like the curve, should be defined in local space. Slab will " .. "then translate the points to the current cursor position.") Slab.NewLine() Slab.Text("Mode") Slab.SameLine() if Slab.BeginComboBox('DrawShapes_Polygon_Mode', {Selected = DrawShapes_Polygon_Mode}) then for I, V in ipairs(DrawShapes_Modes) do if Slab.TextSelectable(V) then DrawShapes_Polygon_Mode = V end end Slab.EndComboBox() end Slab.Polygon(DrawShapes_Polygon, {Color = {0, 0, 1, 1}, Mode = DrawShapes_Polygon_Mode}) end local DrawWindow_X = 900 local DrawWindow_Y = 100 local DrawWindow_W = 200 local DrawWindow_H = 200 local DrawWindow_Title = "A" local DrawWindow_TitleH = nil local DrawWindow_TitleAlignmentX = 'center' local DrawWindow_TitleAlignmentY = 'center' local DrawWindow_TitleAlignmentX_Options = {'left', 'center', 'right'} local DrawWindow_TitleAlignmentY_Options = {'top', 'center', 'bottom'} local DrawWindow_ResetLayout = false local DrawWindow_ResetSize = false local DrawWindow_AutoSizeWindow = true local DrawWindow_AllowResize = true local DrawWindow_AllowMove = true local DrawWindow_AllowFocus = true local DrawWindow_Border = 4.0 local DrawWindow_BgColor = nil local DrawWindow_BgColor_ChangeColor = false local DrawWindow_NoOutline = false local DrawWindow_Constrain = false local DrawWindow_SizerFilter = {} local DrawWindow_SizerFiltersOptions = { N = true, S = true, E = true, W = true, NW = true, NE = true, SW = true, SE = true, } local function DrawWindow_SizerCheckBox(Key) if Slab.CheckBox(DrawWindow_SizerFiltersOptions[Key], Key) then DrawWindow_SizerFiltersOptions[Key] = not DrawWindow_SizerFiltersOptions[Key] end end local function DrawWindow() -- Ensure a valid height. This could be due to a first run. DrawWindow_TitleH = DrawWindow_TitleH or Slab.GetStyle().Font:getHeight() Slab.Textf( "Windows are the basis for which all controls are rendered on and for all user interactions to occur. This area will contain information on the " .. "various options that a window can take and what their expected behaviors will be. The window rendered to the right of this window will be affected " .. "by the changes to the various parameters.") Slab.NewLine() Slab.Separator() Slab.Textf( "The title of the window can be customized. If no title exists, then the title bar is not rendered and the window can not be moved. There is also an " .. "option, AllowMove, to disable movement even with the title bar. The position of the window can be constrained to the viewport through the 'ConstrainPosition' " .. "option. The height of the title bar is also adjustable. The default height will be the height of the current font.") Slab.NewLine() Slab.Text("Title") Slab.SameLine() if Slab.Input('DrawWindow_Title', {Text = DrawWindow_Title, ReturnOnText = false}) then DrawWindow_Title = Slab.GetInputText() end if Slab.CheckBox(DrawWindow_AllowMove, "Allow Move") then DrawWindow_AllowMove = not DrawWindow_AllowMove end if Slab.CheckBox(DrawWindow_Constrain, "Constrain Position To Viewport") then DrawWindow_Constrain = not DrawWindow_Constrain end Slab.Text("Height") Slab.SameLine() if Slab.Input('DrawWindow_TitleHeight', {Text = DrawWindow_TitleH, ReturnOnText = false}) then DrawWindow_TitleH = Slab.GetInputNumber() end Slab.NewLine() Slab.Textf("The text alignment of the title can also be changed.") Slab.Text("Horizontal") Slab.SameLine() if Slab.BeginComboBox('DrawWindow_TitleAlignmentX', {Selected = DrawWindow_TitleAlignmentX}) then for I, V in ipairs(DrawWindow_TitleAlignmentX_Options) do if Slab.TextSelectable(V) then DrawWindow_TitleAlignmentX = V end end Slab.EndComboBox() end Slab.SameLine() Slab.Text("Vertical") Slab.SameLine() if Slab.BeginComboBox('DrawWindow_TitleAlignmentY', {Selected = DrawWindow_TitleAlignmentY}) then for I, V in ipairs(DrawWindow_TitleAlignmentY_Options) do if Slab.TextSelectable(V) then DrawWindow_TitleAlignmentY = V end end Slab.EndComboBox() end Slab.NewLine() Slab.Separator() Slab.Textf( "The default position of the window can be set with the X and Y options. The window can be moved from this position but the parameter values stay the same " .. "as the window keeps track of any delta changes from the starting position. The window can be reset to the default position as described later on below.") Slab.NewLine() Slab.Text("X") Slab.SameLine() if Slab.Input('DrawWindow_X', {Text = tostring(DrawWindow_X), NumbersOnly = true, ReturnOnText = false}) then DrawWindow_X = Slab.GetInputNumber() DrawWindow_ResetLayout = true end Slab.SameLine() Slab.Text("Y") Slab.SameLine() if Slab.Input('DrawWindow_Y', {Text = tostring(DrawWindow_Y), NumbersOnly = true, ReturnOnText = false}) then DrawWindow_Y = Slab.GetInputNumber() DrawWindow_ResetLayout = true end Slab.NewLine() Slab.Separator() Slab.Textf( "The size of the window can be specified. However, windows by default are set to auto size with the AutoSizeWindow option, which resizes the window only when " .. "controls are added to the window. If this option is disabled, then the W and H parameters will be applied to the window.\n" .. "Similar to the window position, the window's size delta changes are stored by the window. The window's size can be reset to the default with the ResetSize " .. "option.") Slab.NewLine() Slab.Text("W") Slab.SameLine() if Slab.Input('DrawWindow_W', {Text = tostring(DrawWindow_W), NumbersOnly = true, ReturnOnText = false, MinNumber = 0}) then DrawWindow_W = Slab.GetInputNumber() DrawWindow_ResetSize = true end Slab.SameLine() Slab.Text("H") Slab.SameLine() if Slab.Input('DrawWindow_H', {Text = tostring(DrawWindow_H), NumbersOnly = true, ReturnOnText = false, MinNumber = 0}) then DrawWindow_H = Slab.GetInputNumber() DrawWindow_ResetSize = true end if Slab.CheckBox(DrawWindow_AutoSizeWindow, "Auto Size Window") then DrawWindow_AutoSizeWindow = not DrawWindow_AutoSizeWindow end Slab.NewLine() Slab.Separator() Slab.Textf( "Windows can be resized onluy if the AutoSizeWindow option is set false. By default, all sides and corners of a window can be resized, but this can be " .. "modified by specifying which directions are allowed to be resized. There is also an option to completely disable resizing with the AllowResize option. " .. "Below is a list of options that are available.") Slab.NewLine() if Slab.CheckBox(DrawWindow_AllowResize, "Allow Resize") then DrawWindow_AllowResize = not DrawWindow_AllowResize end DrawWindow_SizerCheckBox('N') DrawWindow_SizerCheckBox('S') DrawWindow_SizerCheckBox('E') DrawWindow_SizerCheckBox('W') DrawWindow_SizerCheckBox('NW') DrawWindow_SizerCheckBox('NE') DrawWindow_SizerCheckBox('SW') DrawWindow_SizerCheckBox('SE') local FalseCount = 0 DrawWindow_SizerFilter = {} for K, V in pairs(DrawWindow_SizerFiltersOptions) do if V then table.insert(DrawWindow_SizerFilter, K) else FalseCount = FalseCount + 1 end end if FalseCount == 0 then DrawWindow_SizerFilter = {} end Slab.NewLine() Slab.Separator() Slab.Textf( "Windows gain focus when the user clicks within the region of the window. When the window gains focus, it is brought to the top of the window stack. " .. "Through the AllowFocus option, a window may have this behavior turned off.") Slab.NewLine() if Slab.CheckBox(DrawWindow_AllowFocus, "Allow Focus") then DrawWindow_AllowFocus = not DrawWindow_AllowFocus end Slab.NewLine() Slab.Separator() Slab.Textf( "Windows have a border defined which is how much space there is between the edges of the window and the contents of the window.") Slab.NewLine() Slab.Text("Border") Slab.SameLine() if Slab.Input('DrawWindow_Border', {Text = tostring(DrawWindow_Border), NumbersOnly = true, ReturnOnText = false, MinNumber = 0}) then DrawWindow_Border = Slab.GetInputNumber() end Slab.NewLine() Slab.Separator() Slab.Textf( "The ResetSize and ResetLayout options for windows will reset any delta changes to a window's position or size. It is recommended to only pass " .. "in true for these options on a single frame if resetting the position or size is desired.") Slab.NewLine() if Slab.Button("Reset Layout") then DrawWindow_ResetLayout = true end Slab.SameLine() if Slab.Button("Reset Size") then DrawWindow_ResetSize = true end Slab.NewLine() Slab.Separator() Slab.Textf( "The background color of the window can be modified. Along with modifying the color, the outline of the window can be set to drawn or hidden." .. "Hiding the outline and setting the background to be transparent will make only the controls be rendered within the window.") if DrawWindow_BgColor == nil then DrawWindow_BgColor = Slab.GetStyle().WindowBackgroundColor end if Slab.Button("Change Backgound Color") then DrawWindow_BgColor_ChangeColor = true end if Slab.CheckBox(DrawWindow_NoOutline, "No Outline") then DrawWindow_NoOutline = not DrawWindow_NoOutline end if DrawWindow_BgColor_ChangeColor then local Result = Slab.ColorPicker({Color = DrawWindow_BgColor}) if Result.Button ~= 0 then DrawWindow_BgColor_ChangeColor = false if Result.Button == 1 then DrawWindow_BgColor = Result.Color end end end Slab.BeginWindow('DrawWindow_Example', { Title = DrawWindow_Title, TitleH = DrawWindow_TitleH, TitleAlignX = DrawWindow_TitleAlignmentX, TitleAlignY = DrawWindow_TitleAlignmentY, X = DrawWindow_X, Y = DrawWindow_Y, W = DrawWindow_W, H = DrawWindow_H, ResetLayout = DrawWindow_ResetLayout, ResetSize = DrawWindow_ResetSize, AutoSizeWindow = DrawWindow_AutoSizeWindow, SizerFilter = DrawWindow_SizerFilter, AllowResize = DrawWindow_AllowResize, AllowMove = DrawWindow_AllowMove, AllowFocus = DrawWindow_AllowFocus, Border = DrawWindow_Border, BgColor = DrawWindow_BgColor, NoOutline = DrawWindow_NoOutline, ConstrainPosition = DrawWindow_Constrain }) Slab.Text("Hello World") Slab.EndWindow() DrawWindow_ResetLayout = false DrawWindow_ResetSize = false end local DrawTooltip_CheckBox = false local DrawTooltip_Radio = 1 local DrawTooltip_ComboBox_Items = {"Button", "Check Box", "Combo Box", "Image", "Input", "Text", "Tree"} local DrawTooltip_ComboBox_Selected = "Button" local DrawTooltip_Input = "This is an input box." local function DrawTooltip() Slab.Textf( "Slab offers tooltips to be rendered when the user has hovered over the control for a period of time. Not all controls are currently supported, " .. "and this window will show examples for tooltips on the supported controls.") Slab.NewLine() Slab.Button("Button", {Tooltip = "This is a button."}) Slab.NewLine() if Slab.CheckBox(DrawTooltip_CheckBox, "Check Box", {Tooltip = "This is a check box."}) then DrawTooltip_CheckBox = not DrawTooltip_CheckBox end Slab.NewLine() for I = 1, 3, 1 do if Slab.RadioButton("Radio " .. I, {SelectedIndex = DrawTooltip_Radio, Index = I, Tooltip = "This is radio button " .. I}) then DrawTooltip_Radio = I end end Slab.NewLine() if Slab.BeginComboBox('DrawTooltip_ComboBox', {Selected = DrawTooltip_ComboBox_Selected, Tooltip = "This is a combo box."}) then for I, V in ipairs(DrawTooltip_ComboBox_Items) do if Slab.TextSelectable(V) then DrawTooltip_ComboBox_Selected = V end end Slab.EndComboBox() end Slab.NewLine() Slab.Image('DrawTooltip_Image', {Path = DrawImage_Path, Tooltip = "This is an image."}) Slab.NewLine() if Slab.Input('DrawTooltip_Input', {Text = DrawTooltip_Input, Tooltip = DrawTooltip_Input}) then DrawTooltip_Input = Slab.GetInputText() end Slab.NewLine() if Slab.BeginTree('DrawTooltip_Tree_Root', {Label = "Root", Tooltip = "This is the root tree item."}) then Slab.BeginTree('DrawTooltip_Tree_Child', {Label = "Child", Tooltip = "This is the child tree item.", IsLeaf = true}) Slab.EndTree() end Slab.NewLine() Slab.Button("MultiLine Tooltip", {Tooltip = "This is a multi-line tooltip.\nThis is the second line."}) end local DrawStats_SetPosition = false local DrawStats_EncodeIterations = 20 local DrawStats_EncodeLength = 500 local function DrawStats() Slab.Textf( "The Slab API offers functions that track the performance of desired sections of code. With these functions coupled together with the debug " .. "performance window, end-users will be able to see bottlenecks located within their code base quickly. To display the performance window, " .. "call the SlabDebug.Performance function.") Slab.NewLine() Slab.Separator() if not DrawStats_SetPosition then SlabDebug.Performance_SetPosition(800.0, 175.0) DrawStats_SetPosition = true end Slab.Textf( "This page has an example of capturing the performance of encoding data. The iterations and length can be changed to show how the performance is " .. "impacted when these values change.") Slab.NewLine() Slab.Text("Iterations") Slab.SameLine() if Slab.Input('DrawStats_EncodeIterations', {Text = tostring(DrawStats_EncodeIterations), ReturnOnText = false, NumbersOnly = true, MinNumber = 0}) then DrawStats_EncodeIterations = Slab.GetInputNumber() end Slab.SameLine() Slab.Text("Length") Slab.SameLine() if Slab.Input('DrawStats_EncodeLength', {Text = tostring(DrawStats_EncodeLength), ReturnOnText = false, NumbersOnly = true, MinNumber = 0}) then DrawStats_EncodeLength = Slab.GetInputNumber() end local StatHandle = Slab.BeginStat('Encode', 'Slab Test') for I = 1, DrawStats_EncodeIterations, 1 do local LengthStatHandle = Slab.BeginStat('Encode Length', 'Slab Test') local Data = "" for J = 1, DrawStats_EncodeLength, 1 do local Byte = love.math.random(255) Data = Data .. string.char(Byte) end love.data.encode('string', 'hex', Data) Slab.EndStat(LengthStatHandle) end Slab.EndStat(StatHandle) SlabDebug.Performance() end local DrawLayout_AlignX = 'left' local DrawLayout_AlignY = 'top' local DrawLayout_AlignRowY = 'top' local DrawLayout_AlignX_Options = {'left', 'center', 'right'} local DrawLayout_AlignY_Options = {'top', 'center', 'bottom'} local DrawLayout_Radio = 1 local DrawLayout_Input = "Input Control" local DrawLayout_ListBox_Selected = 1 local DrawLayout_Columns = 3 local function DrawLayout() Slab.Textf( "The layout API allows for controls to be grouped together and aligned to a specific position based on the window. " .. "These controls can be aligned to the left, the center, or the right part of a window horizontally. They can also " .. "be aligned to the top, the center, or the bottom vertically in a window. Multiple controls can be declared on the " .. "same line and the API will properly align on the controls on the same line. Below are examples of how this API can " .. "be utilized.") Slab.NewLine() Slab.Separator() Slab.Textf( "The below example shows how controls can be aligned within a window. Use the below options to dictate where the next " .. "set of controls are aligned.") Slab.NewLine() Slab.BeginLayout('DrawLayout_Options', {AlignX = 'center'}) Slab.Text("AlignX") Slab.SameLine() if Slab.BeginComboBox('DrawLayout_AlignX', {Selected = DrawLayout_AlignX}) then for I, V in ipairs(DrawLayout_AlignX_Options) do if Slab.TextSelectable(V) then DrawLayout_AlignX = V end end Slab.EndComboBox() end Slab.SameLine() Slab.Text("AlignY") Slab.SameLine() if Slab.BeginComboBox('DrawLayout_AlignY', {Selected = DrawLayout_AlignY}) then for I, V in ipairs(DrawLayout_AlignY_Options) do if Slab.TextSelectable(V) then DrawLayout_AlignY = V end end Slab.EndComboBox() end Slab.SameLine() Slab.Text("AlignRowY") Slab.SameLine() if Slab.BeginComboBox('DrawLayout_AlignRowY', {Selected = DrawLayout_AlignRowY}) then for I, V in ipairs(DrawLayout_AlignY_Options) do if Slab.TextSelectable(V) then DrawLayout_AlignRowY = V end end Slab.EndComboBox() end Slab.EndLayout() Slab.NewLine() Slab.BeginLayout('DrawLayout_General', {AlignX = DrawLayout_AlignX, AlignY = DrawLayout_AlignY, AlignRowY = DrawLayout_AlignRowY}) Slab.Button("Button 1") Slab.SameLine() Slab.Button("Button 2", {W = 150}) Slab.Button("Button") Slab.SameLine() Slab.Button("Button", {W = 50, H = 50, Tooltip = "This is a large button."}) Slab.SameLine() Slab.Button("Button") Slab.NewLine() Slab.Text("New Lines are supported too.") Slab.EndLayout() Slab.NewLine() Slab.Separator() Slab.Textf( "Controls can also be expanded in the width and height. Only controls that can have their size modified through the API " .. "will be affected by these options. The controls that will be affected are buttons, combo boxes (only the width), " .. "input controls, and list boxes. Non-expandable controls such as text can be mixed in with the controls and the size " .. "of the controls will be adjusted accordingly.") Slab.NewLine() Slab.BeginLayout('DrawLayout_Expand', {ExpandW = true, ExpandH = true}) Slab.Button("OK") Slab.SameLine() Slab.Text("Hello") Slab.SameLine() Slab.Input('DrawLayout_ExpandInput') Slab.SameLine() if Slab.BeginComboBox('DrawLayout_ExpandComboBox') then Slab.EndComboBox() end Slab.SameLine() Slab.BeginListBox('DrawLayout_ExpandListBox', {H = 0}) Slab.EndListBox() Slab.Button("Cancel") Slab.EndLayout() Slab.NewLine() Slab.Separator() Slab.Textf( "Controls can be layed out in columns. The 'Columns' option is a number that tells the layout how many columns to allocate for " .. "positioning the controls. The 'SetLayoutColumn' function sets the current active column and all controls will be placed within " .. "the bounds of that column.") Slab.NewLine() Slab.BeginLayout('DrawLayout_Columns_Options', {AlignX = 'center'}) Slab.Text("Columns") Slab.SameLine() if Slab.Input('DrawLayout_Columns_Input', {Text = tostring(DrawLayout_Columns), ReturnOnText = false, MinNumber = 1, NumbersOnly = true}) then DrawLayout_Columns = Slab.GetInputNumber() end Slab.EndLayout() Slab.NewLine() Slab.BeginLayout('DrawLayout_Columns', {Columns = DrawLayout_Columns, AlignX = 'center'}) for I = 1, DrawLayout_Columns, 1 do Slab.SetLayoutColumn(I) Slab.Text("Column " .. I) Slab.Text("This is a very long string") end Slab.EndLayout() end local DrawFonts_Roboto = nil local DrawFonts_Roboto_Path = SLAB_FILE_PATH .. "/Internal/Resources/Fonts/Roboto-Regular.ttf" local function DrawFonts() if DrawFonts_Roboto == nil then DrawFonts_Roboto = love.graphics.newFont(DrawFonts_Roboto_Path, 18) end Slab.Textf( "Fonts can be pushed to a stack to alter the rendering of any text. All controls will use this pushed font until " .. "the font is popped from the stack, using the last pushed font or the default font. Below is an example of font " .. "being pushed to the stack to render a single text control and then being popped before the next text control.") Slab.NewLine() Slab.PushFont(DrawFonts_Roboto) Slab.Text("This text control is using the Roboto font with point size of 18.") Slab.PopFont() Slab.NewLine() Slab.Text("This text control is using the default font.") end local function DrawScroll() Slab.Textf( "The scroll speed can be modified through the SetScrollSpeed API call. There is also an API function to retrieve " .. "the current speed.") Slab.NewLine() Slab.Text("Speed") Slab.SameLine() if Slab.Input('DrawScroll_Speed', {Text = tostring(Slab.GetScrollSpeed()), ReturnOnText = false, NumbersOnly = true}) then Slab.SetScrollSpeed(Slab.GetInputNumber()) end Slab.NewLine() Slab.BeginListBox('DrawScroll_List') for I = 1, 25, 1 do Slab.Text("Item " .. I) end Slab.EndListBox() end local DrawShader_Object = nil local DrawShader_Time = 0.0 local DrawShader_Source = [[extern number time; vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 pixel_coords) { vec4 TexColor = Texel(texture, texture_coords); return vec4((1.0+sin(time))/2.0, abs(cos(time)), abs(sin(time)), 1.0) * TexColor; }]] local DrawShader_Highlight = { ['vec2'] = {0, 0, 1, 1}, ['vec3'] = {0, 0, 1, 1}, ['vec4'] = {0, 0, 1, 1}, ['mat4'] = {0, 0, 1, 1} } local function DrawShader() if DrawShader_Object == nil then DrawShader_Object = love.graphics.newShader(DrawShader_Source) end DrawShader_Time = DrawShader_Time + love.timer.getDelta() if DrawShader_Object ~= nil then DrawShader_Object:send("time", DrawShader_Time) end Slab.Textf( "Shader effects can be applied to any control through the PushShader/PopShader API calls. Any controls created after " .. "a PushShader call will have its effects applied. The next PopShader call will disable the current effect and apply " .. "the previous shader on the stack if one is present. The shader object to be pushed must be managed by the user and must be " .. "valid when Slab.Draw is called. Below is an example of a shader effect that changes the pixel color over time.") Slab.NewLine() local W, H = Slab.GetWindowActiveSize() local Options = { Text = DrawShader_Source, ReturnOnText = false, MultiLine = true, W = W, H = 150, Highlight = DrawShader_Highlight } Slab.Input('DrawShader_Source', Options) if Slab.Button('Compile') then DrawShader_Source = Slab.GetInputText(); if DrawShader_Object ~= nil then DrawShader_Object:release() end DrawShader_Object = love.graphics.newShader(DrawShader_Source) end Slab.NewLine() Slab.PushShader(DrawShader_Object) Slab.Image('DrawShader_Image', {Path = DrawImage_Path}) Slab.Text("Text") Slab.Button("Button") Slab.PopShader() end local function DrawMessages() Slab.Textf( "Slab has a messaging system that will gather any messages generated by the API and ensure these messages are only " .. "displayed a single time in the console. The messages may be generated if the developer is using a deprecated function " .. "or deprecated options for a control. The API offers a way to disable this system by passing 'NoMessages' to the args of " .. "Slab.Initialize. The API also offers a function to retrieve all gathered messages. Below will display all messages " .. "gathered since the start of this application.") Slab.NewLine() local Messages = Slab.GetMessages() Slab.BeginLayout('DrawMessages_ListBox_Layout', {ExpandW = true, ExpandH = true}) Slab.BeginListBox('DrawMessages_ListBox') for I, V in ipairs(Messages) do Slab.BeginListBoxItem('DrawMessages_Item_' .. I) Slab.Text(V) Slab.EndListBoxItem() end Slab.EndListBox() Slab.EndLayout() end local SlabTest_Options = {Title = "Slab", AutoSizeWindow = false, W = 800.0, H = 600.0, IsOpen = true} function SlabTest.MainMenuBar() if Slab.BeginMainMenuBar() then if Slab.BeginMenu("File") then if Slab.MenuItemChecked("Show Test Window", SlabTest_Options.IsOpen) then SlabTest_Options.IsOpen = not SlabTest_Options.IsOpen end if Slab.MenuItem("Quit", { Hint = "alt+f4" }) then love.event.quit() end Slab.EndMenu() end SlabDebug.Menu() Slab.EndMainMenuBar() end end local Categories = { {"Overview", DrawOverview}, {"Window", DrawWindow}, {"Buttons", DrawButtons}, {"Text", DrawText}, {"Check Box", DrawCheckBox}, {"Radio Button", DrawRadioButton}, {"Menus", DrawMenus}, {"Combo Box", DrawComboBox}, {"Input", DrawInput}, {"Image", DrawImage}, {"Cursor", DrawCursor}, {"List Box", DrawListBox}, {"Tree", DrawTree}, {"Dialog", DrawDialog}, {"Interaction", DrawInteraction}, {"Shapes", DrawShapes}, {"Tooltips", DrawTooltip}, {"Stats", DrawStats}, {"Layout", DrawLayout}, {"Fonts", DrawFonts}, {"Scroll", DrawScroll}, {"Shaders", DrawShader}, {"Messages", DrawMessages} } local Selected = nil function SlabTest.Begin() local StatHandle = Slab.BeginStat('Slab Test', 'Slab Test') SlabTest.MainMenuBar() if Selected == nil then Selected = Categories[1] end Slab.BeginWindow('SlabTest', SlabTest_Options) local W, H = Slab.GetWindowActiveSize() if Slab.BeginComboBox('Categories', {Selected = Selected[1], W = W}) then for I, V in ipairs(Categories) do if Slab.TextSelectable(V[1]) then Selected = Categories[I] end end Slab.EndComboBox() end Slab.Separator() if Selected ~= nil and Selected[2] ~= nil then Selected[2]() end Slab.EndWindow() SlabDebug.Begin() Slab.EndStat(StatHandle) end return SlabTest ================================================ FILE: Style.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Config = require(SLAB_PATH .. '.Internal.Core.Config') local Cursor = require(SLAB_PATH .. '.Internal.Core.Cursor') local FileSystem = require(SLAB_PATH .. '.Internal.Core.FileSystem') local Utility = require(SLAB_PATH .. '.Internal.Core.Utility') local API = {} local Styles = {} local StylePaths = {} local DefaultStyles = {} local CurrentStyle = "" local FontStack = {} local Style = { Font = nil, FontSize = 14, MenuColor = {0.2, 0.2, 0.2, 1.0}, ScrollBarColor = {0.4, 0.4, 0.4, 1.0}, ScrollBarHoveredColor = {0.8, 0.8, 0.8, 1.0}, SeparatorColor = {0.5, 0.5, 0.5, 0.7}, WindowBackgroundColor = {0.2, 0.2, 0.2, 1.0}, WindowTitleFocusedColor = {0.26, 0.53, 0.96, 1.0}, WindowCloseBgColor = {0.64, 0.64, 0.64, 1.0}, WindowCloseColor = {0.0, 0.0, 0.0, 1.0}, ButtonColor = {0.55, 0.55, 0.55, 1.0}, RadioButtonSelectedColor = {0.2, 0.2, 0.2, 1.0}, ButtonHoveredColor = {0.7, 0.7, 0.7, 1.0}, ButtonPressedColor = {0.8, 0.8, 0.8, 1.0}, ButtonDisabledTextColor = {0.35, 0.35, 0.35, 1.0}, CheckBoxSelectedColor = {0.0, 0.0, 0.0, 1.0}, CheckBoxDisabledColor = {0.35, 0.35, 0.35, 1.0}, TextColor = {0.875, 0.875, 0.875, 1.0}, TextDisabledColor = {0.45, 0.45, 0.45, 1.0}, TextHoverBgColor = {0.5, 0.5, 0.5, 1.0}, TextURLColor = {0.2, 0.2, 1.0, 1.0}, ComboBoxColor = {0.4, 0.4, 0.4, 1.0}, ComboBoxHoveredColor = {0.55, 0.55, 0.55, 1.0}, ComboBoxDropDownColor = {0.4, 0.4, 0.4, 1.0}, ComboBoxDropDownHoveredColor = {0.55, 0.55, 0.55, 1.0}, ComboBoxArrowColor = {1.0, 1.0, 1.0, 1.0}, InputBgColor = {0.4, 0.4, 0.4, 1.0}, InputEditBgColor = {0.6, 0.6, 0.6, 1.0}, InputSelectColor = {0.14, 0.29, 0.53, 0.4}, InputSliderColor = {0.1, 0.1, 0.1, 1.0}, MultilineTextColor = {0.0, 0.0, 0.0, 1.0}, ListBoxBgColor = {0.0, 0.0, 0.0, 0.0}, WindowRounding = 2.0, WindowBorder = 4.0, WindowTitleH = 0.0, ButtonRounding = 2.0, CheckBoxRounding = 2.0, ComboBoxRounding = 2.0, InputBgRounding = 2.0, ScrollBarRounding = 2.0, Indent = 14.0, MenuPadH = 0.0, MenuItemPadH = 0.0, API = API } function API.Initialize() local StylePath = "/Internal/Resources/Styles/" local Path = SLAB_FILE_PATH .. StylePath -- Use love's filesystem functions to support both packaged and unpackaged builds local Items = love.filesystem.getDirectoryItems(Path) local StyleName = nil for I, V in ipairs(Items) do if string.find(V, Path, 1, true) == nil then V = Path .. V end local LoadedStyle = API.LoadStyle(V, false, true) if LoadedStyle ~= nil then local Name = FileSystem.GetBaseName(V, true) if StyleName == nil then StyleName = Name end end end if not API.SetStyle("Dark") then API.SetStyle(StyleName) end Style.Font = love.graphics.newFont(Style.FontSize) API.PushFont(Style.Font) Cursor.SetNewLineSize(Style.Font:getHeight()) end function API.LoadStyle(Path, Set, IsDefault) local Contents, Error = Config.LoadFile(Path, IsDefault) if Contents ~= nil then local Name = FileSystem.GetBaseName(Path, true) Styles[Name] = Contents StylePaths[Name] = Path if IsDefault then table.insert(DefaultStyles, Name) end if Set then API.SetStyle(Name) end else print("Failed to load style '" .. Path .. "'.\n" .. Error) end return Contents end function API.SetStyle(Name) if Name == nil then return false end local Other = Styles[Name] if Other ~= nil then CurrentStyle = Name for K, V in pairs(Style) do local New = Other[K] if New ~= nil then if type(V) == "table" then Utility.CopyValues(Style[K], New) else Style[K] = New end end end return true else print("Style '" .. Name .. "' is not loaded.") end return false end function API.GetStyleNames() local Result = {} for K, V in pairs(Styles) do table.insert(Result, K) end return Result end function API.GetCurrentStyleName() return CurrentStyle end function API.CopyCurrentStyle(Path) local NewStyle = Utility.Copy(Styles[CurrentStyle]) local Result, Error = Config.Save(Path, NewStyle) if Result then local NewStyleName = FileSystem.GetBaseName(Path, true) Styles[NewStyleName] = NewStyle StylePaths[NewStyleName] = Path API.SetStyle(NewStyleName) else print("Failed to create new style at path '" .. Path "'. " .. Error) end end function API.SaveCurrentStyle() API.StoreCurrentStyle() local Path = StylePaths[CurrentStyle] local Settings = Styles[CurrentStyle] local Result, Error = Config.Save(Path, Settings) if not Result then print("Failed to save style '" .. CurrentStyle .. "'. " .. Error) end end function API.StoreCurrentStyle() Utility.CopyValues(Styles[CurrentStyle], Style) end function API.IsDefaultStyle(Name) return Utility.Contains(DefaultStyles, Name) end function API.PushFont(Font) if Font ~= nil then Style.Font = Font table.insert(FontStack, 1, Font) end end function API.PopFont() if #FontStack > 1 then table.remove(FontStack, 1) Style.Font = FontStack[1] end end return Style ================================================ FILE: changelog.txt ================================================ v0.9.0 ========== [Stats] Added GetStats and CalculateStats. See fixed #91 (@flamendless) [Window]: Added minimize/maximize button in title bar to hide/show contents #100 (@flamendless) [Slab Logo]: Added Slab logo. Credits to ig@haven.graphicdesigns [Window]: Allow horizontal scrolling when there is no vertical scrolling possible #87 (@flamendless) [Dock]: Dock now follows Window's option for W and H #92 (@flamendless) [Dock]: Allow to programmatically set a window to a dock #92 (@flamendless) [Input]: Added NeedDrag option to InputNumberSlider [Button]: Added VLines option for multiline button label #81 (@flamendless) [Layout]: Added GetCurrentColumnIndex #89 (@flamendless) [Properties]: Improved Properties to use ordered table #93 (@flamendless) [Mouse]: Read mouse arguments out of Args table #94 (@idbrii) BUGFIXES ========== [Dialog]: MessageBox expands if there are multiple buttons #99 (@flamendless) [Image]: Cache not updating correctly when Image object is passed #98 (@flamendless) [ContextMenu]: Context Menu now works with void spaces again #96 (@flamendless) [Tree]: Not returning IsOpen flag correctly #90 (@flamendless) v0.8.0 ========== [API]: Make Update and Draw functions private. [Android]: Receive feedback and make fixes to errant behavior. Applies to issue #35. [Frame]: API calls to setup frames (child regions in Dear ImGui). Can hook into Regions module and expose some functionality from this for this feature. #46. [LayoutManager]: Right aligned controls are flush on the edge. Should flush with the window border. [Mouse/Keyboard]: Allow multiple 'Update' calls. This will conflict with the async input processing. Might need to keep a cache of the last async update and always apply that to every 'Update' call. BUGS ========== [Combo Box]: Selectable text not expanding to full width of window. [File Dialog]: Excess right border. [List Box]: View transform pops while parent window is resizing when scroll is in non-zero position. [List Box]: Region size not reset when list items change. [List Box]: Window not properly auto-sized. [Scroll Bar]: Color flicker when clicking and releasing on scroll bar. [Tooltip]: Gains focus when displayed. Focus should not be allowed. [Tree]: View transform not correct with content size. Content size may be incorrect. [Window]: Clamp window moving to viewport. TODO ========== [Animation]: Initial animation support. [API]: Re-organize functions in categories and update comment. [API]: Get/Set cursor padding. [API]: Utilitze 'args' parameter for configuring docks, save state. [Column]: Investigate moving Column rendering to its own draw channel within an active batch to reduce scissor calls. [Column]: Move separator with mouse. Combo Box: Easy API with table of possible values. Context Menu: Investigate checking hot item and last item of window. Controls: Convert all window bounds check to use region. [Date Time]: Initial implementation for a date/time control. Dialog: Function to close all dialog boxes. Dialog: Modal/Non-modal dialogs. [Dock]: Flickers when using a custom love.run function with a fixed update interval. [Documentation]: Command-line option to just dump all functions listed in API table. [Error]: Dialog that shows any error messages. [Graph]: Initial line graph implementation. [Input]: Drag select word versus character. [Input]: Auto-grow control with MulitLine. [Input]: Range highlight using start and end tokens. [Keyboard]: Option to specify key as scan code. License: Show license for Kenney.nl assets. Main Menu Bar: Assert if called within a window context. [Menu]: Radio button like menu items. Region: Clamp position check with owning window's size. [Scroll Bar]: Velocity based movement for mouse wheel scrolling. [Scroll Bar]: Focus owning window if scroll bar is clicked. [Slab]: Move all love.* calls into interfaces for portability with other Lua based frameworks. SlabDebug: Show image stats. [SlabDebug]: Window management tools. [Stats]: Draw time draw command. [Stats]: Simple API access with list of stat names to display. Style: Add documentation for each style property. [Style]: API documentation. [Tab]: Initial implementation of a container holding Window instances with tabs for each. [Table]: Initial implementation. [Text]: Tooltip support. [Text]: Stats for text objects. [Text]: Deprecate SelectOnHover option. [Tree]: Fix for BeginContextMenuItem for root and children items. Window: Menu support. Window: Ids should be used as keys. Window: Deprecate window bounds values and use region directly. [Window]: Move default border size to Style. [Window]: Allow arrow key movements of window if option is set. [Window]: IsWindowOpen API function. Widgets: Track owning window's alpha channel. Widgets: Forward Options parameter to sub widgets. ================================================ FILE: conf.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] function love.conf(t) t.window.title = "Slab" t.window.width = 1280 t.window.height = 720 t.console = true end ================================================ FILE: init.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] -- Global path used in all modules in this library SLAB_PATH = ... ---@type Slab local Slab = require(SLAB_PATH .. '.API') ---@type Slab return Slab ================================================ FILE: main.lua ================================================ --[[ MIT License Copyright (c) 2019-2021 Love2D Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] local Slab = require 'Slab' local SlabTest = require 'SlabTest' local dontInterceptEventHandlers = true; function love.load(args) love.graphics.setBackgroundColor(0.07, 0.07, 0.07) Slab.Initialize(args, dontInterceptEventHandlers) if dontInterceptEventHandlers then setCustomHandlers() end end function love.update(dt) Slab.Update(dt) SlabTest.Begin() end function love.draw() Slab.Draw() end function _quit() Slab.OnQuit() end function _keypressed(key, scancode, isrepeat) Slab.OnKeyPressed(key, scancode, isrepeat) end function _keyreleased(key, scancode) Slab.OnKeyReleased(key, scancode) end function _textinput(text) Slab.OnTextInput(text) end function _wheelmoved(x, y) Slab.OnWheelMoved(x, y) end function _mousemoved(x, y, dx, dy, istouch) Slab.OnMouseMoved(x, y, dx, dy, istouch) end function _mousepressed( x, y, button, istouch, presses) Slab.OnMousePressed( x, y, button, istouch, presses) end function _mousereleased( x, y, button, istouch, presses) Slab.OnMouseReleased( x, y, button, istouch, presses) end function setCustomHandlers() love.handlers['quit'] = _quit; love.handlers['keypressed'] = _keypressed; love.handlers['keyreleased'] = _keyreleased; love.handlers['textinput'] = _textinput; love.handlers['mousemoved'] = _mousemoved; love.handlers['mousepressed'] = _mousepressed; love.handlers['mousereleased'] = _mousereleased; love.handlers['wheelmoved'] = _wheelmoved; end