Basalt is intended to be an easy-to-understand UI Framework designed for CC:Tweaked - a popular minecraft mod. For more information about CC:Tweaked, checkout the project's wiki or download.
Note: Basalt is still under developement and you may find bugs!
Check out the wiki for more information.
If you have questions, feel free to join the discord server: discord.gg/yNNnmBVBpE.
When labels are used with XML, they terminate unexpectedly. This occurs whether or not a space is present in the string, leading me to believe that the XML has a default size parameter that it sets, instead of calculating the size from the label text.
I tested this on CraftOS PC, as well as in 1.19.2 CC:Tweaked.
MRE:
local basalt = require("basalt")
basalt.createFrame():loadLayout("test.xml")
basalt.autoUpdate()
Again... I'm now sure if this is a bug, or expected behaviour. However, I expected the 'a' to appear in the first input, and then the focus be set to B.
Screenshots
Clip.onKey.mp4
Checklist
[x] I am running the latest version.
Tick the box if you are running the latest version!
Attempt to debug a thread by displaying a value using basalt.debug crashes the program.
Computercraft client
CraftOSPC
Relevant log output
I wish this was copypastable
basalt.lua:2209: Thread Error Occured - cannot resume running coroutine
2209 in eventHandler
713 in eventHandler
71 in cab (one of the minified functions)
this was in an xpcall
80 in autoUpdate
Is your feature request related to a problem? Please describe.
It Is Extremely Annoying when I Have To Go To The Frame Section In The Docs To Find Examples And New Users Would Not Know Where To Find Them
Describe the solution you'd like
I Think The Examples Should Be Moved From The Frame Section To Somewhere Else
Describe alternatives you've considered
I have Considered Adding Something To Tell The User Where The Examples Are But I Think It Would Still Be Too Hard To Find
Is your feature request related to a problem? Please describe.
It seems like basalt.autoUpdate() does not return true errors upon failure. Instead, it prints the error itself, and returns normally. This causes some things that depend on true errors to fail, such as pcall or xpcall.
This effectively makes implementing custom backtraces for basalt impossible. As a real example of this, running basalt inside of mbs will not print the backtrace, as mbs expects the program to raise an actual error upon failure.
The main usage I have for this is to log the backtrace:
In the code above, log_traceback and the code inside the if will never execute, regardless of the failure status of autoUpdate.
Minimal Working Example
localfilePath="/basalt.lua"ifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", ""))
endlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame()
localthread=main:addThread()
localfunctionyourCustomHandler()
whiletruedoos.sleep(1)
error("Horrible error") --Errorendendthread:start(yourCustomHandler)
localsuccess, err=pcall(basalt.autoUpdate)
ifsuccessthen-- Currently, it'll follow this path, as though the execution succeeded.print('Success')
else-- I wish it would follow this path, which is the failure path.print('Error: ', err)
end
Describe the solution you'd like
Calling error with the message displayed passed as an argument that can be catched.
local basalt = dofile("lib/basalt")
local mainFrame = basalt.createFrame("mainFrame"):show()
local label = mainFrame:addLabel("label"):setSize(45,1):setValue("This is a label"):setTextAlign("center","center"):show()
basalt.autoUpdate()
This errors: test.lua:3: attempt to call method 'setTextAlign' (a nil value)
The previous position of the Input's cursor is maintained when a setValue call is followed by a setFocus call, even when the input is empty.
Minimal Working Example
--Basalt configurated installerlocalfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
localinput=main:addInput('input')
:setPosition(1, 1)
:setSize(20, 1)
:setValue('Some Text')
localbutton=main:addButton('button')
:setPosition(1, 2)
:setSize(20, 1)
:setValue('Button')
:onClickUp(function()
-- Important partinput:setValue('')
input:setFocus()
end)
basalt.autoUpdate()
Clip.input.cursor.mp4
Steps to reproduce the behavior:
Type something in the Input
Clear it with input:setValue('')
Focus on it with input:setFocus()
Expected behavior
The cursor would return to the rightmost side of the input.
Using :wait before one of the build-in animations (:size, :position, offset, etc) will not wait before executing the animation.
Minimal Working Example
-- Basalt configurated installerlocalfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
localW, H=main:getSize()
-- Frame using built-in animation (size) will not properly wait.localframe_not_working=main:addFrame("frame_not_working")
frame_not_working:setSize(math.floor(W/2)-2, H-2)
frame_not_working:setPosition(2, 2)
frame_not_working:setBackground(colors.black)
frame_not_working:setBorder(colors.white)
localanimation_not_working=main:addAnimation("animation_not_working")
:setObject(frame_not_working)
:setMode("linear")
-- This will fail. It will not wait 3 seconds.
:wait(3)
:size(math.floor(W/2)-2, 0, 0.5)
:onDone(function()
frame_not_working:remove()
end)
-- Frame avoiding built-in animation (manually resizing) will wait.localframe_working=main:addFrame("frame_working")
frame_working:setSize(math.floor(W/2)-2, H-2)
frame_working:setPosition(math.floor(W/2)+1, 2)
frame_working:setBackground(colors.black)
frame_working:setBorder(colors.white)
localanimation_working=main:addAnimation("animation_working")
:setObject(frame_working)
-- This will succeed, it will wait 3 seconds.
:wait(3)
for_=1, H-3doanimation_working:add(function()
frame_working:setSize(0, -1, 'r')
end)
:wait(0.01)
endanimation_working:onDone(function()
frame_working:remove()
end)
-- Execute.animation_not_working:play()
animation_working:play()
basalt.autoUpdate()
output.mp4
Expected behavior
Wait the amount specified in :wait before starting built-in animation.
When using buttons in XML, if the text is set to only numbers, Basalt crashes. This bug only happens when you create buttons via XML.
If the text field has any non-numeric character the bug doesn't happen.
To Reproduce
Steps to reproduce the behavior:
Create a XML layout with a button with the text "123"
Add this layout to a frame.
Run.
Crash.
Expected behavior
It should just show the numbers, as it does when you programmatically create a button.
Checklist
[x] I am running the latest version.
Tick the box if you are running the latest version!
Currently, if you have multiple dropdowns in a frame, they are prioritized in the order they were added (last in the top).
This means that if you are adding a bunch of dropdowns in a column (perhaps using a for loop), the dropdown box of the ones above will be overshadowed by the label of the ones below.
Minimal Working Example
-- Basalt configurated installerlocalfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
fori=1, 3dolocalfg_color, bg_colorifi%2==0thenfg_color=colors.blackbg_color=colors.whiteelsefg_color=colors.whitebg_color=colors.blackendmain:addDropdown('dropdown_'..i)
:setPosition(20, 6+ (2*i))
:setSize(10, 1)
:addItem('Option 1')
:addItem('Option 2')
:addItem('Option 3')
:addItem('Option 4')
:addItem('Option 5')
:setForeground(fg_color)
:setBackground(bg_color)
:selectItem(1)
endbasalt.autoUpdate()
output.mp4
Desired Solution
For me, a reasonable solution would be to prioritize rendering the dropdown box over the dropdown label whenever they are in the same index.
Alternative Solutions
From basalt's part:
Reverse the priority order (i.e. make later added items less prioritized).
I wouldn't like this solution, as I would expect items added later to be rendered above items added beforehand, including dropdowns.
Prioritize rendering active dropdowns.
I wouldn't like this solution, as I wouldn't expect a dropdown to be "brought to the front" just because it was clicked. For example, in the setup below, the dropdown stays behind after being clicked. That's the behaviour I would expect, and the dropdown getting "popped" up would be weird in that scenario.
output.mp4
From my part:
Reversing the for loop. (i.e. for i = 3, 1, -1 do)
Manually setting the index. (i.e. :setZIndex(3 - i))
Both of these workarounds resolve the issue.
output.mp4
Additional context
Although the workarounds (from my part) are quite simple, I think it's reasonable for a developer using the API to assume the dropdowns boxes (on the same level) would be prioritized over labels.
As an example, in the Windows API for GUI, it's true (and expected) that if you add a couple of dropdowns in a column one after another, when you click on the first dropdown, its box won't be overshadowed by the dropdowns below it. I think it's reasonable to assume the same behaviour in basalt.
Edit: I now realize this is probably a bug report, not a feature request... oh well ยฏ\_(ใ)_/ยฏ
Making a frame with borders 1-wide creates seemingly undefined behaviour (in terms of how the borders will be drawn). In general, only one side is drawn at a time, leading to the other side being "open".
Minimal Working Example
-- Basalt configurated installerlocalfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
localW, H=main:getSize()
localframe=main:addFrame("frame")
frame:setSize(W-2, math.floor(H/2))
frame:setPosition(2, 5)
frame:setBackground(colors.black)
frame:setBorder(colors.white)
localanimation=main:addAnimation("animation")
:setObject(frame)
:size(1, math.floor(H/2), 0.5, 0.5)
animation:play()
basalt.autoUpdate()
output.mp4
Desired Solution
When becoming one-wide, a frame with border of color X should be completely filled by color X (simulating drawing on both sides of the border).
Minimal Working Example (Simulation)
-- Basalt configurated installerlocalfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
localW, H=main:getSize()
localframe=main:addFrame("frame")
frame:setSize(W-2, math.floor(H/2))
frame:setPosition(2, 5)
frame:setBackground(colors.black)
frame:setBorder(colors.white)
localanimation=main:addAnimation("animation")
:setObject(frame)
:size(2, math.floor(H/2), 0.5, 0.5)
:onDone(function()
frame:setSize(1, math.floor(H/2))
frame:setBackground(colors.white)
end)
animation:play()
basalt.autoUpdate()
localfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
localfunctionvisual_button(btn)
btn:onClick(function(self) btn:setBackground(colors.black) btn:setForeground(colors.lightGray) end)
btn:onClickUp(function(self) btn:setBackground(colors.gray) btn:setForeground(colors.black) end)
btn:onLoseFocus(function(self) btn:setBackground(colors.gray) btn:setForeground(colors.black) end)
endlocalworking_frame=main:addFrame("working_frame")
:setPosition(2, 2)
:setSize(20, 10)
:setBackground(colors.black)
-- This will work fine (notice that it's done before adding the children).
:setZIndex(1000)
localworking_input=working_frame:addInput("input")
:setPosition(2, 2)
:setSize(18, 1)
:setDefaultText("Search Text")
localworking_button=working_frame:addButton("button")
:setPosition(2, 4)
:setSize(18, 1)
:setValue("Search")
visual_button(working_button)
localnot_working_frame=main:addFrame("not_working_frame")
:setPosition(23, 2)
:setSize(20, 10)
:setBackground(colors.black)
localnot_working_input=not_working_frame:addInput("input")
:setPosition(2, 2)
:setSize(18, 1)
:setDefaultText("Search Text")
localnot_working_button=not_working_frame:addButton("button")
:setPosition(2, 4)
:setSize(18, 1)
:setValue("Search")
visual_button(not_working_button)
-- This will effectively disable the children (notice that it's done after adding the children).not_working_frame:setZIndex(1000)
main:show()
basalt.autoUpdate()
Expected behavior
Children being interactable after changing the ZIndex of parent.
I really like the way the roblox documentation does this, it lists the methods the object itself has (like currently done), but beneath that it has dropdowns for "Inherited from <X>"
Others
The current way does work, but having something like this will reduce the need to click back and forth through multiple pages.
I also notice that sometimes the In addition to the A and B methods, X objects have the following methods: statement is sometimes missing base class methods (currently I am looking at Input, which is missing ChangeableObject).
See error: Basalt error: .../basaltDraw.lua:202: bad argument (string expected, got nil)
Expected behavior
A button with the text "Hello" should appear with a blue border only along the bottom
Additional context
Changing these four lines (138-141) in Object.lua seems to fix it! Just had to wrap the xmlValue(...) return with colors[...], like the other surrounding code does.
Is your feature request related to a problem? Please describe.
I am frustrated cause its difficult to add support for another image format to Basalt (cimg2 in my case).
Describe the solution you'd like
I would like for there to be some kind of more modular way of handling images, perhaps with a separate library.
Describe alternatives you've considered
Modifying most Basalt code just so it might work.
I've installed basalt and set up my first program following the tutorial.
I've set up a button by using
local btnGate = controlfram
:addButton()
:setPosition(2,2)
:setText("[ OPEN ]")
:setBackground(colors.yellow)
I originally had the onclick set up as part of the call chain, but have movied it to be it's own section
btnGate:onClick(function(self,event,button,x,y)
basalt.debug("CLICK EVENT")
if (event == "mouse_click") and (button == 1) then
basalt.debug("CLICKED")
open("gate") --My function call for my gates
end
end)
after this I have the basalt.autoUpdate() call.
The program starts up, and I can drag the movableframe around like it's supposed to, but clicking a button never does anything. And the debug events are never called. I'm not sure why.
I think it would be cool if we could implement something like Bigfont by Wojbie or something similar for labels.
I have an example of BigFont beeing used, see image below:
Here you can see the "PixelTech INC" sized big, using BigFont, and under the "maybe?", using normal text.
I can imagine the api to be something like this: label:setFontSize(font size), where font size is 1 for normal, 2 for bigger, 3 for even bigger, etc, etc.
update = function(event, ...)
local args = {...}
local function f()
basaltUpdateEvent(event, table.unpack(args))
end
local ok, err = xpcall(f, debug.traceback)
if not(ok)then
basaltError(err)
return
end
end,
and it works fine now
Checklist
[ ] I am running the latest version.
Tick the box if you are running the latest version!
As the title says, the docs contain a Basalt.setTheme() function which appears to no longer exist.
When I load Basalt and run the Basalt.setTheme() from the Lua prompt (or any other file) I get the following error (even if I pass a correct table):
** Possible solutions **
Remove the function from the docs.
Update the docs to contain the new alternative.
Resurrect the old setTheme() function.
** Others **
I have downloaded the recent Minified/Packed Version a few minutes ago.
** Checklist **
[ X ] I am looking at the latest version of the docs.
Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
Parsing XML (or HTML) can be a challenge in Lua. Having the ability to register custom XML parsers will allow people to do this.
Describe the solution you'd like
A clear and concise description of what you want to happen.
The creation of a system that allows people to make custom XML parsers which support custom tags. Each custom tag should be predefined.
Example lua:
-- This function will allow people to do what they want if a tag is parsed for example add it to a layout.-- This function will be called for all tags. Parent tags (tags with child tags) will have this function run first on them, this could allow custom data to be passed to child tags.localfunctionparse(tag)
localname=tag.name-- the name of the taglocalattrs=tag.attrs-- the tag's attributeslocalchildren=tag.children-- the tag's children (a table of other tags in the same format) (or a string if it is just text e.g. `<p>text here</p>`)ifname=="p" thenifnottag.inDivthenframe:addLabel():setText(children) -- amusing `frame` is a `Frame` and `children` is a `string`endendifname=="div" thenfork,vinpairs(children) dov.inDiv=true-- set a custom value on the children of this divendendendlocaltags= {
p= { -- define a tag called "p"children="string" -- this will allow only strings inside a `p` if other types are found (like and other tag) then there will be an error. Single numbers should be parsed as a string.
},
div= { -- define a tag called "div"children="any" -- allow anything inside a div
}
}
localparser=basalt.makeParser(tags, parse)
-- The parser object shall work like the normal XML parser with the same `:loadLayout`. In addition there should be a `:loadString` which takes the XML code as a string instead of a file name.
Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
Copying your existing XML parser and modifying it.
Additional context
Add any other context or screenshots about the feature request here.
This will allow me to make a better HTML parser for my CC Browser and have a very nice UI.
It seems we're limited to using the standard colors provided by the colors library. Using colors created with colors.packRGB results in an error, as the color lookup in the tHex library fails. I'd Like to be able to use custom colors for gradients in heat graphs and darkening buttons when clicked.
Are there plans to support custom colors, and is this even supported by the monitors?
--Basalt configurated installerlocalfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
localbutton=main:addTextfield('button')
:setSize(20, 5)
:addLine('something')
basalt.autoUpdate()
Expected behavior
If the first line is empty, addLine would add the text to the first line.
[X] I am running the latest version.
Tick the box if you are running the latest version!
When you use os.pullEvent in a thread the filter is ignored.
To Reproduce
localbasalt=require("basalt")
localmain=basalt.createFrame():setTheme({FrameBG=colors.lightGray, FrameFG=colors.black})
localthread=main:addThread()
thread:start(function()
whiletruedoprint(textutils.serialize({os.pullEvent("key_up")}))
-- Now I'm not sure if this code would work-- Just because the basalt event handler isn't running when I try to start this thread-- but my point still stands-- if it does work, this will error. It will instead catch one of the basalt events, one that has functions in the table returned therefor crashing the program.endend)
basalt.autoUpdate()
Expected behavior
The example above wouldn't crash, because it would only ever resume on "key_up" events
Screenshots
N/A
Additional context
N/A
Checklist
[ ] I am running the latest version.
Tick the box if you are running the latest version!
I am running the DEV branch, from when you told me to update it
If the text (or value) in the button is longer than its width, the text will overflow the button's bounding box.
Minimal Working Example
-- Basalt configurated installerlocalfilePath="/basalt.lua" --here you can change the file path default: basaltifnot(fs.exists(filePath))thenshell.run("pastebin run ESs1mg7P packed true "..filePath:gsub(".lua", "")) -- this is an alternative to the wget commandendlocalbasalt=require(filePath:gsub(".lua", ""))
localmain=basalt.createFrame("mainFrame")
main:addButton('button')
:setPosition(20, 8)
:setSize(10, 3)
:setValue('some very very long name')
basalt.autoUpdate()
Expected behavior
The text would be "cut off" the bounding box of the button.
localbasalt=require("basalt") -- we need basalt herelocalmain=basalt.createFrame():setTheme({FrameBG=colors.lightGray, FrameFG=colors.black}) -- we change the default bg and fg color for frameslocalsub= { -- here we create a table where we gonna add some framesmain:addFrame():setPosition(1, 2):setSize("{parent.w}", "{parent.h - 1}"), -- obviously the first one should be shown on program startmain:addFrame():setPosition(1, 2):setSize("{parent.w}", "{parent.h - 1}"):hide(),
main:addFrame():setPosition(1, 2):setSize("{parent.w}", "{parent.h - 1}"):hide(),
}
localfunctionopenSubFrame(id) -- we create a function which switches the frame for usif(sub[id]~=nil)thenfork,vinpairs(sub)dov:hide()
endsub[id]:show()
endendlocalmenubar=main:addMenubar():setScrollable() -- we create a menubar in our main frame.
:setSize("{parent.w}")
:onChange(function(self, val)
openSubFrame(self:getItemIndex()) -- here we open the sub frame based on the table indexend)
:addItem("Example 1")
:addItem("Example 2")
:addItem("Example 3")
-- Now we can change our sub frames, if you want to access a sub frame just use sub[subid], some examples:sub[1]:addButton():setPosition(2, 2)
sub[2]:addLabel():setText("Hello World!"):setPosition(2, 2)
sub[3]:addLabel():setText("Now we're on example 3!"):setPosition(2, 2)
sub[3]:addButton():setText("No functionality"):setPosition(2, 4):setSize(18, 3)
basalt.autoUpdate()
But it throws an error dynamicValues.lua:51: attempt to preform arithmetic on a table value
To Reproduce
Steps to reproduce the behavior:
Clone the repository move Basalt as basalt
Create a file called test.lua in computer
Paste the given code
Run the code and get the error
Expected behavior
Example code should work
Screenshots
Additional context
Checklist
[X] I am running the latest version.
Tick the box if you are running the latest version!
Calling the addObject function with a directory path doesn't seem to do anything at all - custom object modules are not required.
It looks like all this function does is add the provided path to newObjects in main.lua, but newObjects is only ever visted when main.lua is first evaluated (i.e. when the module is required, before the addObject function can even be called).
To Reproduce
Steps to reproduce the behavior:
Add a custom object module to a folder that has a debug print or something at the top
Pass the folder to basalt.addObject("...") in a test program
Run the program and observe that the custom modules are not imported
Expected behavior
The custom object modules would be imported and registered as object types in Basalt.