@ -79,6 +79,7 @@ Properties:
|
||||
| hint | string | yes | yes | text to display when nothing is entered |
|
||||
| caret | int | yes | yes | carriage position. `textbox.caret = -1` will set the position to the end of the text |
|
||||
| editable | bool | yes | yes | text mutability |
|
||||
| edited | bool | yes | yes\* | is text edited since the last set / edited status reset |
|
||||
| multiline | bool | yes | yes | multiline support |
|
||||
| lineNumbers | bool | yes | yes | display line numbers |
|
||||
| textWrap | bool | yes | yes | automatic text wrapping (only with multiline: "true") |
|
||||
@ -87,6 +88,8 @@ Properties:
|
||||
| syntax | string | yes | yes | syntax highlighting ("lua" - Lua) |
|
||||
| markup | string | yes | yes | text markup language ("md" - Markdown) |
|
||||
|
||||
\* - false only
|
||||
|
||||
Methods:
|
||||
|
||||
| Method | Description |
|
||||
|
||||
@ -110,6 +110,8 @@ Inner text - initially entered text
|
||||
- `supplier` - text supplier (called every frame)
|
||||
- `consumer` - lua function that receives the entered text. Called only when input is complete
|
||||
- `sub-consumer` - lua function-receiver of the input text. Called during text input or deletion.
|
||||
- `oncontrolkey` - lua function called for combinations of the form (Ctrl + ?). The codepoint of the second key is given as the first argument.
|
||||
The key code for comparison can be obtained via `input.keycode("key_name")`
|
||||
- `autoresize` - automatic change of element size (default - false). Does not affect font size.
|
||||
- `multiline` - allows display of multiline text.
|
||||
- `text-wrap` - allows automatic text wrapping (works only with multiline: "true")
|
||||
|
||||
@ -79,6 +79,7 @@ document["worlds-panel"]:clear()
|
||||
| hint | string | да | да | текст, отображаемый, когда ничего не введено |
|
||||
| caret | int | да | да | позиция каретки. `textbox.caret = -1` установит позицию в конец текста |
|
||||
| editable | bool | да | да | изменяемость текста |
|
||||
| edited | bool | да | да\* | был ли изменён текст с последней установки/сброса свойства |
|
||||
| multiline | bool | да | да | поддержка многострочности |
|
||||
| lineNumbers | bool | да | да | отображение номеров строк |
|
||||
| textWrap | bool | да | да | автоматический перенос текста (только при multiline: "true") |
|
||||
@ -87,6 +88,8 @@ document["worlds-panel"]:clear()
|
||||
| syntax | string | да | да | подсветка синтаксиса ("lua" - Lua) |
|
||||
| markup | string | да | да | язык разметки текста ("md" - Markdown) |
|
||||
|
||||
\* - только false
|
||||
|
||||
Методы:
|
||||
|
||||
| Метод | Описание |
|
||||
|
||||
@ -111,6 +111,8 @@
|
||||
- `supplier` - поставщик текста (вызывается каждый кадр)
|
||||
- `consumer` - lua функция-приемник введенного текста. Вызывается только при завершении ввода
|
||||
- `sub-consumer` - lua функция-приемник вводимого текста. Вызывается во время ввода или удаления текста.
|
||||
- `oncontrolkey` - lua функция вызываемая для сочетаний вида (Ctrl + ?). На вход подаётся числовой код второй клавиши.
|
||||
Код клавиши для сравнения можно получить через `input.keycode("имя_клавиши")`
|
||||
- `autoresize` - автоматическое изменение размера элемента (по-умолчанию - false). Не влияет на размер шрифта.
|
||||
- `multiline` - разрешает отображение многострочного текста.
|
||||
- `text-wrap` - разрешает автоматический перенос текста (работает только при multiline: "true")
|
||||
|
||||
@ -2,17 +2,14 @@
|
||||
<panel interval="0"
|
||||
orientation="horizontal"
|
||||
color="#00000010"
|
||||
size-func="gui.get_viewport()[1]-350,30">
|
||||
size-func="gui.get_viewport()[1],30">
|
||||
<button id="s_chat" size="110,30" onclick="modes:set('chat')">@Chat</button>
|
||||
<button id="s_console" size="110,30" onclick="modes:set('console')">@Console</button>
|
||||
<button id="s_debug" size="110,30" onclick="modes:set('debug')">@Debug</button>
|
||||
</panel>
|
||||
<container pos="0,30" size-func="gui.get_viewport()[1]-350,30" color="#00000020">
|
||||
<label id="title" pos="8,8"></label>
|
||||
</container>
|
||||
|
||||
<container id="logContainer" pos="0,60"
|
||||
size-func="unpack(vec2.add(gui.get_viewport(), {-350,-100}))">
|
||||
size-func="unpack(vec2.add(gui.get_viewport(), {-450,-100}))">
|
||||
<textbox
|
||||
id='log'
|
||||
color='0'
|
||||
@ -20,38 +17,77 @@
|
||||
margin='0'
|
||||
editable='false'
|
||||
multiline='true'
|
||||
size-func="gui.get_viewport()[1]-350,40"
|
||||
size-func="-1,40"
|
||||
gravity="bottom-left"
|
||||
markup="md"
|
||||
></textbox>
|
||||
</container>
|
||||
<container id="editorContainer" pos="0,60" color="#00000080"
|
||||
size-func="unpack(vec2.add(gui.get_viewport(), {-350,-230}))">
|
||||
<textbox
|
||||
id='editor'
|
||||
color='0'
|
||||
autoresize='true'
|
||||
margin='0'
|
||||
padding='5'
|
||||
multiline='true'
|
||||
line-numbers='true'
|
||||
syntax='lua'
|
||||
size-func="gui.get_viewport()[1]-350,40"
|
||||
gravity="top-left"
|
||||
text-wrap='false'
|
||||
scroll-step='50'
|
||||
></textbox>
|
||||
</container>
|
||||
<panel id="traceback" gravity="bottom-left" padding="4" color="#000000A0"
|
||||
max-length="170" size-func="gui.get_viewport()[1]-350,170">
|
||||
</panel>
|
||||
<panel id="problemsLog"
|
||||
color="#00000010"
|
||||
position-func="gui.get_viewport()[1]-350,0"
|
||||
size-func="350,gui.get_viewport()[2]-40"
|
||||
padding="5,15,5,15">
|
||||
<label margin="0,0,0,5">@Problems</label>
|
||||
</panel>
|
||||
<splitbox id="editorRoot" pos="0,30" size-func="-1,gui.get_viewport()[2]-30"
|
||||
orientation="horizontal" split-pos="0.3">
|
||||
<splitbox split-pos="0.75">
|
||||
<panel interval="2" color="0" padding="2">
|
||||
<textbox pos="2" sub-consumer="filter_files"></textbox>
|
||||
<panel id="filesList" color="#00000010" interval="6" padding="4"
|
||||
size-func="-1,-45" pos="2,38">
|
||||
<!-- content is generated in script -->
|
||||
</panel>
|
||||
</panel>
|
||||
<panel id="problemsLog"
|
||||
color="#00000010"
|
||||
padding="5,15,5,15">
|
||||
<label margin="0,0,0,5">@Problems</label>
|
||||
</panel>
|
||||
</splitbox>
|
||||
<splitbox id="editorContainer" split-pos="0.8">
|
||||
<container color="#00000080"
|
||||
onclick="document.editor.focused = true document.editor.caret = -1">
|
||||
<container size-func="-1,30" color="#00000020">
|
||||
<image id="lockIcon" src="gui/lock" tooltip="@Read only"
|
||||
interactive="true" onclick="unlock_access()"
|
||||
color="#FFFFFF80" size="16" pos="4,6"
|
||||
hover-color="#1080FF"></image>
|
||||
<panel orientation="horizontal" gravity="top-right"
|
||||
size="60,16" padding="8" interval="8" color="0">
|
||||
<image id="saveIcon" src="gui/save" tooltip="@Save"
|
||||
enabled="false" interactive="true"
|
||||
hover-color="#1080FF"
|
||||
onclick="save_current_file()"
|
||||
color="#FFFFFF80" size="16"></image>
|
||||
<image id="infoIcon" src="gui/info" tooltip="@editor.info.tooltip"
|
||||
enabled="true" interactive="true"
|
||||
color="#FFFFFF80" size="16"></image>
|
||||
<image id="syncIcon" src="gui/play" tooltip="@Run"
|
||||
enabled="true" interactive="true"
|
||||
hover-color="#1080FF"
|
||||
onclick="run_current_file()"
|
||||
color="#FFFFFF80" size="16"></image>
|
||||
</panel>
|
||||
<label id="title" pos="26,8"></label>
|
||||
</container>
|
||||
<textbox
|
||||
id='editor'
|
||||
pos='0,30'
|
||||
color='0'
|
||||
autoresize='true'
|
||||
margin='0'
|
||||
padding='5'
|
||||
multiline='true'
|
||||
line-numbers='true'
|
||||
oncontrolkey='on_control_combination'
|
||||
syntax='lua'
|
||||
size-func="-1,40"
|
||||
text-wrap='false'
|
||||
scroll-step='50'
|
||||
></textbox>
|
||||
</container>
|
||||
<splitbox orientation="horizontal" split-pos="0.4">
|
||||
<panel id="traceback" padding="4" color="#000000A0">
|
||||
</panel>
|
||||
<panel id="output" padding="4" color="#000000A0">
|
||||
</panel>
|
||||
</splitbox>
|
||||
</splitbox>
|
||||
</splitbox>
|
||||
<textbox id='prompt'
|
||||
consumer='submit'
|
||||
margin='0'
|
||||
|
||||
@ -9,6 +9,15 @@ local errors_all = {}
|
||||
local warning_id = 0
|
||||
local error_id = 0
|
||||
|
||||
local writeables = {}
|
||||
local registry = require "core:internal/scripts_registry"
|
||||
local filenames
|
||||
|
||||
local current_file = {
|
||||
filename = "",
|
||||
mutable = nil
|
||||
}
|
||||
|
||||
events.on("core:warning", function (wtype, text, traceback)
|
||||
local full = wtype..": "..text
|
||||
if table.has(warnings_all, full) then
|
||||
@ -45,14 +54,184 @@ events.on("core:error", function (msg, traceback)
|
||||
table.insert(errors_all, full)
|
||||
end)
|
||||
|
||||
local function find_mutable(filename)
|
||||
local packid = file.prefix(filename)
|
||||
if packid == "core" then
|
||||
return
|
||||
end
|
||||
local saved = writeables[packid]
|
||||
if saved then
|
||||
return saved..":"..file.path(filename)
|
||||
end
|
||||
local packinfo = pack.get_info(packid)
|
||||
if not packinfo then
|
||||
return
|
||||
end
|
||||
local path = packinfo.path
|
||||
if file.is_writeable(path) then
|
||||
return file.join(path, file.path(filename))
|
||||
end
|
||||
end
|
||||
|
||||
local function refresh_file_title()
|
||||
if current_file.filename == "" then
|
||||
document.title.text = ""
|
||||
return
|
||||
end
|
||||
local edited = document.editor.edited
|
||||
current_file.modified = edited
|
||||
document.saveIcon.enabled = edited
|
||||
document.title.text = gui.str('File')..' - '..current_file.filename
|
||||
..(edited and ' *' or '')
|
||||
end
|
||||
|
||||
function build_files_list(filenames, selected)
|
||||
local files_list = document.filesList
|
||||
files_list:clear()
|
||||
|
||||
for _, actual_filename in ipairs(filenames) do
|
||||
local filename = actual_filename
|
||||
if selected then
|
||||
filename = filename:gsub(selected, "**"..selected.."**")
|
||||
end
|
||||
local parent = file.parent(filename)
|
||||
local info = registry.get_info(actual_filename)
|
||||
local icon = "file"
|
||||
if info then
|
||||
icon = info.type == "component" and "entity" or info.type
|
||||
end
|
||||
files_list:add(gui.template("script_file", {
|
||||
path = parent .. (parent[#parent] == ':' and '' or '/'),
|
||||
name = file.name(filename),
|
||||
icon = icon,
|
||||
unit = info and info.unit or '',
|
||||
filename = actual_filename
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
function filter_files(text)
|
||||
local filtered = {}
|
||||
for _, filename in ipairs(filenames) do
|
||||
if filename:find(text) then
|
||||
table.insert(filtered, filename)
|
||||
end
|
||||
end
|
||||
build_files_list(filtered, text)
|
||||
end
|
||||
|
||||
function on_control_combination(keycode)
|
||||
if keycode == input.keycode("s") then
|
||||
save_current_file()
|
||||
elseif keycode == input.keycode("r") then
|
||||
run_current_file()
|
||||
end
|
||||
end
|
||||
|
||||
function unlock_access()
|
||||
if current_file.filename == "" then
|
||||
return
|
||||
end
|
||||
pack.request_writeable(file.prefix(current_file.filename),
|
||||
function(token)
|
||||
writeables[file.prefix(current_file.filename)] = token
|
||||
current_file.mutable = token..":"..file.path(current_file.filename)
|
||||
open_file_in_editor(current_file.filename, 0, current_file.mutable)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
function run_current_file()
|
||||
if not current_file.filename then
|
||||
return
|
||||
end
|
||||
local chunk, err = loadstring(document.editor.text, current_file.filename)
|
||||
clear_output()
|
||||
if not chunk then
|
||||
local line, message = err:match(".*:(%d*): (.*)")
|
||||
document.output:add(
|
||||
string.format(
|
||||
"<label color='#FF3030' enabled='false' margin='2'>%s: %s</label>",
|
||||
gui.str("Error at line %{0}"):gsub("%%{0}", line), message)
|
||||
)
|
||||
return
|
||||
end
|
||||
local info = registry.get_info(current_file.filename)
|
||||
local script_type = info and info.type or "file"
|
||||
local unit = info and info.unit
|
||||
save_current_file()
|
||||
|
||||
local func = function()
|
||||
local stack_size = debug.count_frames()
|
||||
xpcall(chunk, function(msg) __vc__error(msg, 1, 1, stack_size) end)
|
||||
end
|
||||
|
||||
local funcs = {
|
||||
block = block.reload_script,
|
||||
item = item.reload_script,
|
||||
world = world.reload_script,
|
||||
hud = hud.reload_script,
|
||||
component = entities.reload_component,
|
||||
module = reload_module,
|
||||
}
|
||||
func = funcs[script_type] or func
|
||||
local output = core.capture_output(function() func(unit) end)
|
||||
document.output:add(
|
||||
string.format(
|
||||
"<label enabled='false' multiline='true' margin='2'>%s</label>",
|
||||
output)
|
||||
)
|
||||
end
|
||||
|
||||
function save_current_file()
|
||||
if not current_file.mutable then
|
||||
return
|
||||
end
|
||||
file.write(current_file.mutable, document.editor.text)
|
||||
current_file.modified = false
|
||||
document.saveIcon.enabled = false
|
||||
document.title.text = gui.str('File')..' - '..current_file.filename
|
||||
document.editor.edited = false
|
||||
end
|
||||
|
||||
function open_file_in_editor(filename, line, mutable)
|
||||
local editor = document.editor
|
||||
local source = file.read(filename):gsub('\t', ' ')
|
||||
editor.text = source
|
||||
editor.focused = true
|
||||
if line then
|
||||
time.post_runnable(function()
|
||||
editor.caret = editor:linePos(line)
|
||||
end)
|
||||
end
|
||||
document.title.text = gui.str('File') .. ' - ' .. filename
|
||||
current_file.filename = filename
|
||||
current_file.mutable = mutable or find_mutable(filename)
|
||||
document.lockIcon.visible = current_file.mutable == nil
|
||||
document.editor.editable = current_file.mutable ~= nil
|
||||
document.saveIcon.enabled = current_file.modified
|
||||
end
|
||||
|
||||
function clear_traceback()
|
||||
local tb_list = document.traceback
|
||||
tb_list:clear()
|
||||
tb_list:add("<label enabled='false' margin='2'>@devtools.traceback</label>")
|
||||
end
|
||||
|
||||
function clear_output()
|
||||
local output = document.output
|
||||
output:clear()
|
||||
output:add("<label enabled='false' margin='2'>@devtools.output</label>")
|
||||
end
|
||||
|
||||
events.on("core:open_traceback", function(traceback_b64)
|
||||
local traceback = bjson.frombytes(base64.decode(traceback_b64))
|
||||
modes:set('debug')
|
||||
|
||||
clear_traceback()
|
||||
|
||||
local tb_list = document.traceback
|
||||
local srcsize = tb_list.size
|
||||
tb_list:clear()
|
||||
tb_list:add("<label enabled='false' margin='2'>@devtools.traceback</label>")
|
||||
for _, frame in ipairs(traceback.frames) do
|
||||
local callback = ""
|
||||
local framestr = ""
|
||||
@ -62,23 +241,12 @@ events.on("core:open_traceback", function(traceback_b64)
|
||||
framestr = frame.source..":"..tostring(frame.currentline).." "
|
||||
if file.exists(frame.source) then
|
||||
callback = string.format(
|
||||
"local editor = document.editor "..
|
||||
"local source = file.read('%s'):gsub('\t', ' ') "..
|
||||
"editor.text = source "..
|
||||
"editor.focused = true "..
|
||||
"time.post_runnable(function()"..
|
||||
"editor.caret = editor:linePos(%s) "..
|
||||
"end)",
|
||||
"open_file_in_editor('%s', %s)",
|
||||
frame.source, frame.currentline-1
|
||||
)
|
||||
else
|
||||
callback = "document.editor.text = 'Could not open source file'"
|
||||
end
|
||||
callback = string.format(
|
||||
"%s document.title.text = gui.str('File')..' - %s'",
|
||||
callback,
|
||||
frame.source
|
||||
)
|
||||
end
|
||||
if frame.name then
|
||||
framestr = framestr.."("..tostring(frame.name)..")"
|
||||
@ -184,7 +352,8 @@ end
|
||||
function set_mode(mode)
|
||||
local show_prompt = mode == 'chat' or mode == 'console'
|
||||
|
||||
document.title.text = ""
|
||||
document.lockIcon.visible = false
|
||||
document.editorRoot.visible = mode == 'debug'
|
||||
document.editorContainer.visible = mode == 'debug'
|
||||
document.logContainer.visible = mode ~= 'debug'
|
||||
|
||||
@ -194,7 +363,6 @@ function set_mode(mode)
|
||||
document.root.color = {0, 0, 0, 128}
|
||||
end
|
||||
|
||||
document.traceback.visible = mode == 'debug'
|
||||
document.prompt.visible = show_prompt
|
||||
if show_prompt then
|
||||
document.prompt.focused = true
|
||||
@ -211,6 +379,17 @@ function on_open(mode)
|
||||
}, function (mode)
|
||||
set_mode(mode)
|
||||
end, mode or "console")
|
||||
|
||||
local files_list = document.filesList
|
||||
|
||||
filenames = registry.filenames
|
||||
table.sort(filenames)
|
||||
build_files_list(filenames)
|
||||
|
||||
document.editorContainer:setInterval(200, refresh_file_title)
|
||||
|
||||
clear_traceback()
|
||||
clear_output()
|
||||
elseif mode then
|
||||
modes:set(mode)
|
||||
end
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
<container id="%{id}" size="32" tooltip="%{text}"
|
||||
onclick="events.emit('core:open_traceback', '%{traceback}')">
|
||||
<image src="gui/%{type}" size="32"/>
|
||||
<container pos="36,2" size="280,32" interactive="false">
|
||||
<label>%{text}</label>
|
||||
</container>
|
||||
<label pos="36,2" sizefunc="-1,-1">%{text}</label>
|
||||
<image src="gui/cross" interactive="true" size="16" gravity="top-right"
|
||||
onclick="document['%{id}']:destruct()"></image>
|
||||
</container>
|
||||
|
||||
12
res/layouts/templates/script_file.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<container size="18">
|
||||
<image src="gui/%{icon}" size="16"></image>
|
||||
<label hover-color='#30A0FF'
|
||||
pos="20,2"
|
||||
interactive="true"
|
||||
onclick='open_file_in_editor("%{filename}")'
|
||||
markup='md'
|
||||
tooltip='%{unit}'
|
||||
sizefunc="-1,-1">
|
||||
[#FFFFFF80]%{path}[#FFFFFFFF]%{name}
|
||||
</label>
|
||||
</container>
|
||||
74
res/modules/internal/scripts_registry.lua
Normal file
@ -0,0 +1,74 @@
|
||||
local export = {}
|
||||
|
||||
local function collect_components(dirname, dest)
|
||||
if file.isdir(dirname) then
|
||||
local files = file.list(dirname)
|
||||
for i, filename in ipairs(files) do
|
||||
if file.ext(filename) == "lua" then
|
||||
table.insert(dest, filename)
|
||||
export.classification[filename] = {
|
||||
type="component",
|
||||
unit=file.prefix(filename)..":"..file.stem(filename)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function collect_scripts(dirname, dest, ismodule)
|
||||
if file.isdir(dirname) then
|
||||
local files = file.list(dirname)
|
||||
for i, filename in ipairs(files) do
|
||||
if file.name(filename) == "components" and not ismodule then
|
||||
collect_components(filename, dest)
|
||||
elseif file.isdir(filename) then
|
||||
collect_scripts(filename, dest)
|
||||
elseif file.ext(filename) == "lua" then
|
||||
table.insert(dest, filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function load_scripts_list()
|
||||
local packs = pack.get_installed()
|
||||
for _, packid in ipairs(packs) do
|
||||
collect_scripts(packid..":modules", export.filenames, true)
|
||||
end
|
||||
for _, filename in ipairs(export.filenames) do
|
||||
export.classification[filename] = {
|
||||
type="module",
|
||||
unit=file.join(file.parent(file.prefix(filename)..":"..
|
||||
filename:sub(filename:find("/")+1)),
|
||||
file.stem(filename))
|
||||
}
|
||||
end
|
||||
for _, packid in ipairs(packs) do
|
||||
collect_scripts(packid..":scripts", export.filenames, false)
|
||||
end
|
||||
end
|
||||
|
||||
function export.build_classification()
|
||||
local classification = {}
|
||||
for id, props in pairs(block.properties) do
|
||||
classification[props["script-file"]] = {type="block", unit=block.name(id)}
|
||||
end
|
||||
for id, props in pairs(item.properties) do
|
||||
classification[props["script-file"]] = {type="item", unit=item.name(id)}
|
||||
end
|
||||
local packs = pack.get_installed()
|
||||
for _, packid in ipairs(packs) do
|
||||
classification[packid..":scripts/world.lua"] = {type="world", unit=packid}
|
||||
classification[packid..":scripts/hud.lua"] = {type="hud", unit=packid}
|
||||
end
|
||||
export.classification = classification
|
||||
export.filenames = {}
|
||||
|
||||
load_scripts_list()
|
||||
end
|
||||
|
||||
function export.get_info(filename)
|
||||
return export.classification[filename]
|
||||
end
|
||||
|
||||
return export
|
||||
@ -24,7 +24,18 @@
|
||||
"misc/snow",
|
||||
"gui/check_mark",
|
||||
"gui/left_arrow",
|
||||
"gui/right_arrow"
|
||||
"gui/right_arrow",
|
||||
"gui/lock",
|
||||
"gui/save",
|
||||
"gui/block",
|
||||
"gui/item",
|
||||
"gui/file",
|
||||
"gui/module",
|
||||
"gui/play",
|
||||
"gui/info",
|
||||
"gui/world",
|
||||
"gui/hud",
|
||||
"gui/entity"
|
||||
],
|
||||
"fonts": [
|
||||
{
|
||||
|
||||
@ -7,7 +7,7 @@ local names = {
|
||||
"hidden", "draw-group", "picking-item", "surface-replacement", "script-name",
|
||||
"ui-layout", "inventory-size", "tick-interval", "overlay-texture",
|
||||
"translucent", "fields", "particles", "icon-type", "icon", "placing-block",
|
||||
"stack-size", "name"
|
||||
"stack-size", "name", "script-file"
|
||||
}
|
||||
for name, _ in pairs(user_props) do
|
||||
table.insert(names, name)
|
||||
@ -61,3 +61,6 @@ end
|
||||
|
||||
cache_names(block)
|
||||
cache_names(item)
|
||||
|
||||
local scripts_registry = require "core:internal/scripts_registry"
|
||||
scripts_registry.build_classification()
|
||||
|
||||
@ -429,6 +429,18 @@ function file.readlines(path)
|
||||
return lines
|
||||
end
|
||||
|
||||
function debug.count_frames()
|
||||
local frames = 1
|
||||
while true do
|
||||
local info = debug.getinfo(frames)
|
||||
if info then
|
||||
frames = frames + 1
|
||||
else
|
||||
return frames - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function debug.get_traceback(start)
|
||||
local frames = {}
|
||||
local n = 2 + (start or 0)
|
||||
@ -463,6 +475,35 @@ function on_deprecated_call(name, alternatives)
|
||||
end
|
||||
end
|
||||
|
||||
function reload_module(name)
|
||||
local prefix, name = parse_path(name)
|
||||
local path = prefix..":modules/"..name..".lua"
|
||||
|
||||
local previous = package.loaded[path]
|
||||
if not previous then
|
||||
debug.log("attempt to reload non-loaded module "..name.." ("..path..")")
|
||||
return
|
||||
end
|
||||
local script, err = load(file.read(path), path)
|
||||
if script == nil then
|
||||
error(err)
|
||||
end
|
||||
local result = script()
|
||||
if not result then
|
||||
return
|
||||
end
|
||||
for i, value in ipairs(result) do
|
||||
previous[i] = value
|
||||
end
|
||||
local copy = table.copy(result)
|
||||
for key, value in pairs(result) do
|
||||
result[key] = nil
|
||||
end
|
||||
for key, value in pairs(copy) do
|
||||
previous[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Load script with caching
|
||||
--
|
||||
-- path - script path `contentpack:filename`.
|
||||
@ -513,9 +554,13 @@ function __scripts_cleanup()
|
||||
end
|
||||
end
|
||||
|
||||
function __vc__error(msg, frame)
|
||||
function __vc__error(msg, frame, n, lastn)
|
||||
if events then
|
||||
events.emit("core:error", msg, debug.get_traceback(1))
|
||||
local frames = debug.get_traceback(1)
|
||||
events.emit(
|
||||
"core:error", msg,
|
||||
table.sub(frames, 1 + (n or 0), lastn and #frames-lastn)
|
||||
)
|
||||
end
|
||||
return debug.traceback(msg, frame)
|
||||
end
|
||||
@ -543,3 +588,23 @@ end
|
||||
function file.prefix(path)
|
||||
return path:match("^([^:]+)")
|
||||
end
|
||||
|
||||
function file.parent(path)
|
||||
local dir = path:match("(.*)/")
|
||||
if not dir then
|
||||
return file.prefix(path)..":"
|
||||
end
|
||||
return dir
|
||||
end
|
||||
|
||||
function file.path(path)
|
||||
local pos = path:find(':')
|
||||
return path:sub(pos + 1)
|
||||
end
|
||||
|
||||
function file.join(a, b)
|
||||
if a[#a] == ':' then
|
||||
return a .. b
|
||||
end
|
||||
return a .. "/" .. b
|
||||
end
|
||||
|
||||
@ -11,7 +11,9 @@ world.delete-confirm=Do you want to delete world forever?
|
||||
world.generators.default=Default
|
||||
world.generators.flat=Flat
|
||||
|
||||
editor.info.tooltip=CTRL+S - Save\nCTRL+R - Run\nCTRL+Z - Undo\nCTRL+Y - Redo
|
||||
devtools.traceback=Traceback (most recent call first)
|
||||
devtools.output=Output
|
||||
|
||||
# Tooltips
|
||||
graphics.gamma.tooltip=Lighting brightness curve
|
||||
|
||||
@ -19,8 +19,16 @@ Problems=Проблемы
|
||||
Monitor=Мониторинг
|
||||
Debug=Отладка
|
||||
File=Файл
|
||||
Read only=Только для чтения
|
||||
Save=Сохранить
|
||||
Grant %{0} pack modification permission?=Выдать разрешение на модификацию пака %{0}?
|
||||
Error at line %{0}=Ошибка на строке %{0}
|
||||
Run=Запустить
|
||||
|
||||
editor.info.tooltip=CTRL+S - Сохранить\nCTRL+R - Запустить\nCTRL+Z - Отменить\nCTRL+Y - Повторить
|
||||
devtools.traceback=Стек вызовов (от последнего)
|
||||
devtools.output=Вывод
|
||||
|
||||
error.pack-not-found=Не удалось найти пакет
|
||||
error.dependency-not-found=Используемая зависимость не найдена
|
||||
pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)?
|
||||
|
||||
BIN
res/textures/gui/block.png
Normal file
|
After Width: | Height: | Size: 114 B |
BIN
res/textures/gui/entity.png
Normal file
|
After Width: | Height: | Size: 150 B |
BIN
res/textures/gui/file.png
Normal file
|
After Width: | Height: | Size: 120 B |
BIN
res/textures/gui/hud.png
Normal file
|
After Width: | Height: | Size: 160 B |
BIN
res/textures/gui/info.png
Normal file
|
After Width: | Height: | Size: 129 B |
BIN
res/textures/gui/item.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
res/textures/gui/lock.png
Normal file
|
After Width: | Height: | Size: 120 B |
BIN
res/textures/gui/module.png
Normal file
|
After Width: | Height: | Size: 150 B |
BIN
res/textures/gui/play.png
Normal file
|
After Width: | Height: | Size: 130 B |
BIN
res/textures/gui/save.png
Normal file
|
After Width: | Height: | Size: 123 B |
BIN
res/textures/gui/world.png
Normal file
|
After Width: | Height: | Size: 178 B |
@ -68,3 +68,5 @@ inline const std::string LAYOUTS_FOLDER = "layouts";
|
||||
inline const std::string SOUNDS_FOLDER = "sounds";
|
||||
inline const std::string MODELS_FOLDER = "models";
|
||||
inline const std::string SKELETONS_FOLDER = "skeletons";
|
||||
|
||||
inline const std::string FONT_DEFAULT = "normal";
|
||||
|
||||
@ -79,6 +79,14 @@ const ContentPackRuntime* Content::getPackRuntime(const std::string& id) const {
|
||||
return found->second.get();
|
||||
}
|
||||
|
||||
ContentPackRuntime* Content::getPackRuntime(const std::string& id) {
|
||||
auto found = packs.find(id);
|
||||
if (found == packs.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return found->second.get();
|
||||
}
|
||||
|
||||
const UptrsMap<std::string, BlockMaterial>& Content::getBlockMaterials() const {
|
||||
return blockMaterials;
|
||||
}
|
||||
|
||||
@ -121,6 +121,14 @@ public:
|
||||
return *found->second;
|
||||
}
|
||||
|
||||
T& require(const std::string& id) {
|
||||
const auto& found = defs.find(id);
|
||||
if (found == defs.end()) {
|
||||
throw std::runtime_error("missing content unit " + id);
|
||||
}
|
||||
return *found->second;
|
||||
}
|
||||
|
||||
const auto& getDefs() const {
|
||||
return defs;
|
||||
}
|
||||
@ -240,6 +248,7 @@ public:
|
||||
const rigging::SkeletonConfig* getSkeleton(const std::string& id) const;
|
||||
const BlockMaterial* findBlockMaterial(const std::string& id) const;
|
||||
const ContentPackRuntime* getPackRuntime(const std::string& id) const;
|
||||
ContentPackRuntime* getPackRuntime(const std::string& id);
|
||||
|
||||
const UptrsMap<std::string, BlockMaterial>& getBlockMaterials() const;
|
||||
const UptrsMap<std::string, ContentPackRuntime>& getPacks() const;
|
||||
|
||||
@ -94,8 +94,9 @@ std::unique_ptr<Content> ContentBuilder::build() {
|
||||
def->rt.surfaceReplacement = content->blocks.require(def->surfaceReplacement).rt.id;
|
||||
if (def->properties == nullptr) {
|
||||
def->properties = dv::object();
|
||||
def->properties["name"] = def->name;
|
||||
}
|
||||
def->properties["name"] = def->name;
|
||||
def->properties["script-file"] = def->scriptFile;
|
||||
}
|
||||
|
||||
for (ItemDef* def : itemDefsIndices) {
|
||||
@ -104,6 +105,7 @@ std::unique_ptr<Content> ContentBuilder::build() {
|
||||
def->properties = dv::object();
|
||||
}
|
||||
def->properties["name"] = def->name;
|
||||
def->properties["script-file"] = def->scriptFile;
|
||||
}
|
||||
|
||||
for (auto& [name, def] : content->generators.getDefs()) {
|
||||
|
||||
@ -394,6 +394,7 @@ void ContentLoader::loadBlock(
|
||||
if (def.hidden && def.pickingItem == def.name + BLOCK_ITEM_SUFFIX) {
|
||||
def.pickingItem = CORE_EMPTY;
|
||||
}
|
||||
def.scriptFile = pack->id + ":scripts/" + def.scriptName + ".lua";
|
||||
}
|
||||
|
||||
void ContentLoader::loadItem(
|
||||
@ -452,6 +453,8 @@ void ContentLoader::loadItem(
|
||||
def.emission[1] = emissionarr[1].asNumber();
|
||||
def.emission[2] = emissionarr[2].asNumber();
|
||||
}
|
||||
|
||||
def.scriptFile = pack->id + ":scripts/" + def.scriptName + ".lua";
|
||||
}
|
||||
|
||||
void ContentLoader::loadEntity(
|
||||
@ -850,25 +853,54 @@ void ContentLoader::load() {
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static void load_scripts(Content& content, ContentUnitDefs<T>& units) {
|
||||
for (const auto& [name, def] : units.getDefs()) {
|
||||
size_t pos = name.find(':');
|
||||
if (pos == std::string::npos) {
|
||||
throw std::runtime_error("invalid content unit name");
|
||||
}
|
||||
const auto runtime = content.getPackRuntime(name.substr(0, pos));
|
||||
const auto& pack = runtime->getInfo();
|
||||
const auto& folder = pack.folder;
|
||||
auto scriptfile = folder / ("scripts/" + def->scriptName + ".lua");
|
||||
if (io::is_regular_file(scriptfile)) {
|
||||
scripting::load_content_script(
|
||||
runtime->getEnvironment(),
|
||||
name,
|
||||
scriptfile,
|
||||
pack.id + ":scripts/" + def->scriptName + ".lua",
|
||||
def->rt.funcsset
|
||||
);
|
||||
}
|
||||
static void load_script(const Content& content, T& def) {
|
||||
const auto& name = def.name;
|
||||
size_t pos = name.find(':');
|
||||
if (pos == std::string::npos) {
|
||||
throw std::runtime_error("invalid content unit name");
|
||||
}
|
||||
const auto runtime = content.getPackRuntime(name.substr(0, pos));
|
||||
const auto& pack = runtime->getInfo();
|
||||
const auto& folder = pack.folder;
|
||||
auto scriptfile = folder / ("scripts/" + def.scriptName + ".lua");
|
||||
if (io::is_regular_file(scriptfile)) {
|
||||
scripting::load_content_script(
|
||||
runtime->getEnvironment(),
|
||||
name,
|
||||
scriptfile,
|
||||
def.scriptFile,
|
||||
def.rt.funcsset
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static void load_scripts(const Content& content, ContentUnitDefs<T>& units) {
|
||||
for (const auto& [_, def] : units.getDefs()) {
|
||||
load_script(content, *def);
|
||||
}
|
||||
}
|
||||
|
||||
void ContentLoader::reloadScript(const Content& content, Block& block) {
|
||||
load_script(content, block);
|
||||
}
|
||||
|
||||
void ContentLoader::reloadScript(const Content& content, ItemDef& item) {
|
||||
load_script(content, item);
|
||||
}
|
||||
|
||||
void ContentLoader::loadWorldScript(ContentPackRuntime& runtime) {
|
||||
const auto& pack = runtime.getInfo();
|
||||
const auto& folder = pack.folder;
|
||||
io::path scriptFile = folder / "scripts/world.lua";
|
||||
if (io::is_regular_file(scriptFile)) {
|
||||
scripting::load_world_script(
|
||||
runtime.getEnvironment(),
|
||||
pack.id,
|
||||
scriptFile,
|
||||
pack.id + ":scripts/world.lua",
|
||||
runtime.worldfuncsset
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -881,16 +913,8 @@ void ContentLoader::loadScripts(Content& content) {
|
||||
const auto& folder = pack.folder;
|
||||
|
||||
// Load main world script
|
||||
io::path scriptFile = folder / "scripts/world.lua";
|
||||
if (io::is_regular_file(scriptFile)) {
|
||||
scripting::load_world_script(
|
||||
runtime->getEnvironment(),
|
||||
pack.id,
|
||||
scriptFile,
|
||||
pack.id + ":scripts/world.lua",
|
||||
runtime->worldfuncsset
|
||||
);
|
||||
}
|
||||
loadWorldScript(*runtime);
|
||||
|
||||
// Load entity components
|
||||
io::path componentsDir = folder / "scripts/components";
|
||||
foreach_file(componentsDir, [&pack](const io::path& file) {
|
||||
|
||||
@ -77,4 +77,7 @@ public:
|
||||
void load();
|
||||
|
||||
static void loadScripts(Content& content);
|
||||
static void loadWorldScript(ContentPackRuntime& pack);
|
||||
static void reloadScript(const Content& content, Block& block);
|
||||
static void reloadScript(const Content& content, ItemDef& item);
|
||||
};
|
||||
|
||||
@ -15,6 +15,7 @@ using wstringsupplier = std::function<std::wstring()>;
|
||||
using doublesupplier = std::function<double()>;
|
||||
using boolsupplier = std::function<bool()>;
|
||||
using vec2supplier = std::function<glm::vec2()>;
|
||||
using key_handler = std::function<bool(int)>;
|
||||
|
||||
using stringconsumer = std::function<void(const std::string&)>;
|
||||
using wstringconsumer = std::function<void(const std::wstring&)>;
|
||||
|
||||
164
src/devtools/actions.hpp
Normal file
@ -0,0 +1,164 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
class Action {
|
||||
public:
|
||||
virtual ~Action() = default;
|
||||
|
||||
virtual void apply() = 0;
|
||||
virtual void revert() = 0;
|
||||
};
|
||||
|
||||
class InversedAction : public Action {
|
||||
public:
|
||||
InversedAction(std::unique_ptr<Action> action) : action(std::move(action)) {}
|
||||
|
||||
void apply() override {
|
||||
action->revert();
|
||||
}
|
||||
|
||||
void revert() override {
|
||||
action->apply();
|
||||
}
|
||||
private:
|
||||
std::unique_ptr<Action> action;
|
||||
};
|
||||
|
||||
class CombinedAction : public Action {
|
||||
public:
|
||||
CombinedAction(std::vector<std::unique_ptr<Action>> actions)
|
||||
: actions(std::move(actions)) {
|
||||
}
|
||||
|
||||
void apply() override {
|
||||
for (auto& action : actions) {
|
||||
action->apply();
|
||||
}
|
||||
}
|
||||
|
||||
void revert() override {
|
||||
for (int i = actions.size() - 1; i >= 0; i--) {
|
||||
actions[i]->revert();
|
||||
}
|
||||
}
|
||||
private:
|
||||
std::vector<std::unique_ptr<Action>> actions;
|
||||
};
|
||||
|
||||
class ActionsHistory {
|
||||
public:
|
||||
ActionsHistory() {};
|
||||
|
||||
/// @brief Remove all actions available to redo
|
||||
void clearRedo() {
|
||||
if (actionPtr < actions.size()) {
|
||||
actions.erase(actions.begin() + actionPtr, actions.end());
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Store action without applying
|
||||
void store(std::unique_ptr<Action> action, bool reverse=false) {
|
||||
if (lock) {
|
||||
return;
|
||||
}
|
||||
if (reverse) {
|
||||
action = std::make_unique<InversedAction>(std::move(action));
|
||||
}
|
||||
clearRedo();
|
||||
actions.emplace_back(std::move(action));
|
||||
actionPtr++;
|
||||
}
|
||||
|
||||
/// @brief Apply action and store it
|
||||
void apply(std::unique_ptr<Action> action) {
|
||||
if (lock) {
|
||||
return;
|
||||
}
|
||||
clearRedo();
|
||||
lock = true;
|
||||
action->apply();
|
||||
lock = false;
|
||||
actions.emplace_back(std::move(action));
|
||||
actionPtr++;
|
||||
}
|
||||
|
||||
/// @brief Revert the last action
|
||||
/// @return true if any action reverted
|
||||
bool undo() {
|
||||
if (lock || actionPtr == 0) {
|
||||
return false;
|
||||
}
|
||||
auto& action = actions[--actionPtr];
|
||||
lock = true;
|
||||
action->revert();
|
||||
lock = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief Revert the last action
|
||||
/// @return true if any action reapplied
|
||||
bool redo() {
|
||||
if (lock || actionPtr == actions.size()) {
|
||||
return false;
|
||||
}
|
||||
auto& action = actions[actionPtr++];
|
||||
lock = true;
|
||||
action->apply();
|
||||
lock = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief Clear history without reverting actions
|
||||
void clear() {
|
||||
actionPtr = 0;
|
||||
actions.clear();
|
||||
}
|
||||
|
||||
/// @brief Squash last n actions into one CombinedAction
|
||||
/// @param n number of actions to squash
|
||||
void squash(ptrdiff_t n) {
|
||||
if (n < 2) {
|
||||
return;
|
||||
}
|
||||
n = std::min(n, static_cast<ptrdiff_t>(actionPtr));
|
||||
std::vector<std::unique_ptr<Action>> squashing;
|
||||
for (size_t i = actionPtr - n; i < actionPtr; i++) {
|
||||
squashing.emplace_back(std::move(actions[i]));
|
||||
}
|
||||
actions.erase(actions.begin() + actionPtr - n, actions.end());
|
||||
actionPtr -= n;
|
||||
store(std::make_unique<CombinedAction>(std::move(squashing)));
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return actionPtr;
|
||||
}
|
||||
|
||||
/// @brief On destruction squashing actions stored since initialization
|
||||
struct Combination {
|
||||
ActionsHistory& history;
|
||||
size_t historySize;
|
||||
|
||||
Combination(ActionsHistory& history)
|
||||
: history(history), historySize(history.size()) {
|
||||
}
|
||||
|
||||
Combination(const Combination&) = delete;
|
||||
|
||||
Combination(Combination&&) = default;
|
||||
|
||||
~Combination() {
|
||||
history.squash(history.size() - historySize);
|
||||
}
|
||||
};
|
||||
|
||||
Combination beginCombination() {
|
||||
return Combination(*this);
|
||||
}
|
||||
private:
|
||||
std::vector<std::unique_ptr<Action>> actions;
|
||||
size_t actionPtr = 0;
|
||||
bool lock = false;
|
||||
};
|
||||
@ -163,6 +163,9 @@ void Engine::updateHotkeys() {
|
||||
if (Events::jpressed(keycode::F2)) {
|
||||
saveScreenshot();
|
||||
}
|
||||
if (Events::jpressed(keycode::F8)) {
|
||||
gui->toggleDebug();
|
||||
}
|
||||
if (Events::jpressed(keycode::F11)) {
|
||||
settings.display.fullscreen.toggle();
|
||||
}
|
||||
@ -481,6 +484,10 @@ const Content* Engine::getContent() const {
|
||||
return content.get();
|
||||
}
|
||||
|
||||
Content* Engine::getWriteableContent() {
|
||||
return content.get();
|
||||
}
|
||||
|
||||
std::vector<ContentPack> Engine::getAllContentPacks() {
|
||||
auto packs = getContentPacks();
|
||||
packs.insert(packs.begin(), ContentPack::createCore(paths));
|
||||
|
||||
@ -151,6 +151,8 @@ public:
|
||||
/// @brief Get current Content instance
|
||||
const Content* getContent() const;
|
||||
|
||||
Content* getWriteableContent();
|
||||
|
||||
/// @brief Get selected content packs
|
||||
std::vector<ContentPack>& getContentPacks();
|
||||
|
||||
|
||||
@ -262,7 +262,7 @@ void Hud::updateHotbarControl() {
|
||||
}
|
||||
}
|
||||
|
||||
void Hud::updateWorldGenDebugVisualization() {
|
||||
void Hud::updateWorldGenDebug() {
|
||||
auto& level = frontend.getLevel();
|
||||
const auto& chunks = *player.chunks;
|
||||
auto generator =
|
||||
@ -314,7 +314,9 @@ void Hud::update(bool visible) {
|
||||
const auto& chunks = *player.chunks;
|
||||
const auto& menu = gui.getMenu();
|
||||
|
||||
debugPanel->setVisible(debug && visible);
|
||||
debugPanel->setVisible(
|
||||
debug && visible && !(inventoryOpen && inventoryView == nullptr)
|
||||
);
|
||||
|
||||
if (!visible && inventoryOpen) {
|
||||
closeInventory();
|
||||
@ -358,7 +360,7 @@ void Hud::update(bool visible) {
|
||||
|
||||
debugMinimap->setVisible(debug && showGeneratorMinimap);
|
||||
if (debug && showGeneratorMinimap) {
|
||||
updateWorldGenDebugVisualization();
|
||||
updateWorldGenDebug();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -135,7 +135,7 @@ class Hud : public util::ObjectsKeeper {
|
||||
void dropExchangeSlot();
|
||||
|
||||
void showExchangeSlot();
|
||||
void updateWorldGenDebugVisualization();
|
||||
void updateWorldGenDebug();
|
||||
public:
|
||||
Hud(Engine& engine, LevelFrontend& frontend, Player& player);
|
||||
~Hud();
|
||||
|
||||
@ -142,7 +142,7 @@ void Batch2D::rect(
|
||||
bool flippedY,
|
||||
glm::vec4 tint
|
||||
) {
|
||||
if (index + 6*B2D_VERTEX_SIZE >= capacity) {
|
||||
if (index + 6 * B2D_VERTEX_SIZE >= capacity) {
|
||||
flush();
|
||||
}
|
||||
setPrimitive(DrawPrimitive::triangle);
|
||||
@ -230,6 +230,11 @@ void Batch2D::rect(
|
||||
}
|
||||
|
||||
void Batch2D::lineRect(float x, float y, float w, float h) {
|
||||
if (index + 8 * B2D_VERTEX_SIZE >= capacity) {
|
||||
flush();
|
||||
}
|
||||
setPrimitive(DrawPrimitive::line);
|
||||
|
||||
vertex(x, y, 0.0f, 0.0f, color.r, color.g, color.b, color.a);
|
||||
vertex(x, y+h, 0.0f, 1.0f, color.r, color.g, color.b, color.a);
|
||||
|
||||
|
||||
@ -48,10 +48,14 @@ public:
|
||||
void sprite(float x, float y, float w, float h, float skew, int atlasRes, int index, glm::vec4 tint);
|
||||
void point(float x, float y, float r, float g, float b, float a);
|
||||
|
||||
void setColor(glm::vec4 color) {
|
||||
void setColor(const glm::vec4& color) {
|
||||
this->color = color;
|
||||
}
|
||||
|
||||
void setColor(int r, int g, int b, int a=255) {
|
||||
this->color = glm::vec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f);
|
||||
}
|
||||
|
||||
void resetColor() {
|
||||
this->color = glm::vec4(1.0f);
|
||||
}
|
||||
|
||||
@ -38,11 +38,11 @@ bool Font::isPrintableChar(uint codepoint) const {
|
||||
}
|
||||
}
|
||||
|
||||
int Font::calcWidth(const std::wstring& text, size_t length) const {
|
||||
int Font::calcWidth(std::wstring_view text, size_t length) const {
|
||||
return calcWidth(text, 0, length);
|
||||
}
|
||||
|
||||
int Font::calcWidth(const std::wstring& text, size_t offset, size_t length) const {
|
||||
int Font::calcWidth(std::wstring_view text, size_t offset, size_t length) const {
|
||||
return std::min(text.length()-offset, length) * glyphInterval;
|
||||
}
|
||||
|
||||
|
||||
@ -56,14 +56,14 @@ public:
|
||||
/// @param text selected text
|
||||
/// @param length max substring length (default: no limit)
|
||||
/// @return pixel width of the substring
|
||||
int calcWidth(const std::wstring& text, size_t length=-1) const;
|
||||
int calcWidth(std::wstring_view text, size_t length=-1) const;
|
||||
|
||||
/// @brief Calculate text width in pixels
|
||||
/// @param text selected text
|
||||
/// @param offset start of the substring
|
||||
/// @param length max substring length
|
||||
/// @return pixel width of the substring
|
||||
int calcWidth(const std::wstring& text, size_t offset, size_t length) const;
|
||||
int calcWidth(std::wstring_view text, size_t offset, size_t length) const;
|
||||
|
||||
/// @brief Check if character is visible (non-whitespace)
|
||||
/// @param codepoint character unicode codepoint
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include "graphics/core/Batch3D.hpp"
|
||||
#include "graphics/core/Shader.hpp"
|
||||
#include "presets/NotePreset.hpp"
|
||||
#include "constants.hpp"
|
||||
|
||||
TextsRenderer::TextsRenderer(
|
||||
Batch3D& batch, const Assets& assets, const Frustum& frustum
|
||||
@ -44,7 +45,7 @@ void TextsRenderer::renderNote(
|
||||
}
|
||||
opacity = preset.xrayOpacity;
|
||||
}
|
||||
const auto& font = assets.require<Font>("normal");
|
||||
const auto& font = assets.require<Font>(FONT_DEFAULT);
|
||||
|
||||
glm::vec3 xvec = note.getAxisX();
|
||||
glm::vec3 yvec = note.getAxisY();
|
||||
|
||||
@ -11,14 +11,15 @@
|
||||
#include "frontend/UiDocument.hpp"
|
||||
#include "frontend/locale.hpp"
|
||||
#include "graphics/core/Batch2D.hpp"
|
||||
#include "graphics/core/LineBatch.hpp"
|
||||
#include "graphics/core/Shader.hpp"
|
||||
#include "graphics/core/Font.hpp"
|
||||
#include "graphics/core/DrawContext.hpp"
|
||||
#include "window/Events.hpp"
|
||||
#include "window/Window.hpp"
|
||||
#include "window/input.hpp"
|
||||
#include "window/Camera.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
@ -34,12 +35,13 @@ GUI::GUI()
|
||||
|
||||
menu = std::make_shared<Menu>();
|
||||
menu->setId("menu");
|
||||
menu->setZIndex(10);
|
||||
container->add(menu);
|
||||
container->setScrollable(false);
|
||||
|
||||
tooltip = guiutil::create(
|
||||
"<container color='#000000A0' interactive='false' z-index='999'>"
|
||||
"<label id='tooltip.label' pos='2' autoresize='true'></label>"
|
||||
"<label id='tooltip.label' pos='2' autoresize='true' multiline='true' text-wrap='false'></label>"
|
||||
"</container>"
|
||||
);
|
||||
store("tooltip", tooltip);
|
||||
@ -107,7 +109,7 @@ void GUI::actMouse(float delta) {
|
||||
doubleClicked = false;
|
||||
doubleClickTimer += delta + mouseDelta * 0.1f;
|
||||
|
||||
auto hover = container->getAt(Events::cursor, nullptr);
|
||||
auto hover = container->getAt(Events::cursor);
|
||||
if (this->hover && this->hover != hover) {
|
||||
this->hover->setHover(false);
|
||||
}
|
||||
@ -238,6 +240,39 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
if (hover) {
|
||||
Window::setCursor(hover->getCursor());
|
||||
}
|
||||
if (hover && debug) {
|
||||
auto pos = hover->calcPos();
|
||||
const auto& id = hover->getId();
|
||||
if (!id.empty()) {
|
||||
auto& font = assets.require<Font>(FONT_DEFAULT);
|
||||
auto text = util::str2wstr_utf8(id);
|
||||
int width = font.calcWidth(text);
|
||||
int height = font.getLineHeight();
|
||||
|
||||
batch2D->untexture();
|
||||
batch2D->setColor(0, 0, 0);
|
||||
batch2D->rect(pos.x, pos.y, width, height);
|
||||
|
||||
batch2D->resetColor();
|
||||
font.draw(*batch2D, text, pos.x, pos.y, nullptr, 0);
|
||||
}
|
||||
|
||||
batch2D->untexture();
|
||||
auto node = hover->getParent();
|
||||
while (node) {
|
||||
auto pos = node->calcPos();
|
||||
auto size = node->getSize();
|
||||
|
||||
batch2D->setColor(0, 255, 255);
|
||||
batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1);
|
||||
|
||||
node = node->getParent();
|
||||
}
|
||||
// debug draw
|
||||
auto size = hover->getSize();
|
||||
batch2D->setColor(0, 255, 0);
|
||||
batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<UINode> GUI::getFocused() const {
|
||||
@ -252,8 +287,8 @@ void GUI::add(std::shared_ptr<UINode> node) {
|
||||
container->add(std::move(node));
|
||||
}
|
||||
|
||||
void GUI::remove(std::shared_ptr<UINode> node) noexcept {
|
||||
container->remove(std::move(node));
|
||||
void GUI::remove(UINode* node) noexcept {
|
||||
container->remove(node);
|
||||
}
|
||||
|
||||
void GUI::store(const std::string& name, std::shared_ptr<UINode> node) {
|
||||
@ -297,3 +332,7 @@ void GUI::setDoubleClickDelay(float delay) {
|
||||
float GUI::getDoubleClickDelay() const {
|
||||
return doubleClickDelay;
|
||||
}
|
||||
|
||||
void GUI::toggleDebug() {
|
||||
debug = !debug;
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ class DrawContext;
|
||||
class Assets;
|
||||
class Camera;
|
||||
class Batch2D;
|
||||
class LineBatch;
|
||||
|
||||
/*
|
||||
Some info about padding and margin.
|
||||
@ -73,6 +74,7 @@ namespace gui {
|
||||
float doubleClickTimer = 0.0f;
|
||||
float doubleClickDelay = 0.5f;
|
||||
bool doubleClicked = false;
|
||||
bool debug = false;
|
||||
|
||||
void actMouse(float delta);
|
||||
void actFocused();
|
||||
@ -113,7 +115,11 @@ namespace gui {
|
||||
void add(std::shared_ptr<UINode> node);
|
||||
|
||||
/// @brief Remove node from the main container
|
||||
void remove(std::shared_ptr<UINode> node) noexcept;
|
||||
void remove(UINode* node) noexcept;
|
||||
|
||||
void remove(const std::shared_ptr<UINode>& node) noexcept {
|
||||
return remove(node.get());
|
||||
}
|
||||
|
||||
/// @brief Store node in the GUI nodes dictionary
|
||||
/// (does not add node to the main container)
|
||||
@ -144,5 +150,7 @@ namespace gui {
|
||||
|
||||
void setDoubleClickDelay(float delay);
|
||||
float getDoubleClickDelay() const;
|
||||
|
||||
void toggleDebug();
|
||||
};
|
||||
}
|
||||
|
||||
43
src/graphics/ui/elements/BasePanel.hpp
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "Container.hpp"
|
||||
|
||||
namespace gui {
|
||||
class BasePanel : public Container {
|
||||
public:
|
||||
virtual ~BasePanel() = default;
|
||||
|
||||
virtual void setOrientation(Orientation orientation) {
|
||||
this->orientation = orientation;
|
||||
refresh();
|
||||
}
|
||||
|
||||
Orientation getOrientation() const {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
virtual void setPadding(glm::vec4 padding) {
|
||||
this->padding = padding;
|
||||
refresh();
|
||||
}
|
||||
|
||||
glm::vec4 getPadding() const {
|
||||
return padding;
|
||||
}
|
||||
protected:
|
||||
BasePanel(
|
||||
glm::vec2 size,
|
||||
glm::vec4 padding = glm::vec4(0.0f),
|
||||
float interval = 2.0f,
|
||||
Orientation orientation = Orientation::vertical
|
||||
)
|
||||
: Container(std::move(size)),
|
||||
padding(std::move(padding)),
|
||||
interval(interval) {
|
||||
}
|
||||
|
||||
Orientation orientation = Orientation::vertical;
|
||||
glm::vec4 padding;
|
||||
float interval = 2.0f;
|
||||
};
|
||||
}
|
||||
@ -17,9 +17,7 @@ Container::~Container() {
|
||||
Container::clear();
|
||||
}
|
||||
|
||||
std::shared_ptr<UINode> Container::getAt(
|
||||
const glm::vec2& pos, const std::shared_ptr<UINode>& self
|
||||
) {
|
||||
std::shared_ptr<UINode> Container::getAt(const glm::vec2& pos) {
|
||||
if (!isInteractive() || !isEnabled()) {
|
||||
return nullptr;
|
||||
}
|
||||
@ -28,19 +26,19 @@ std::shared_ptr<UINode> Container::getAt(
|
||||
}
|
||||
int diff = (actualLength-size.y);
|
||||
if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) {
|
||||
return UINode::getAt(pos, self);
|
||||
return UINode::getAt(pos);
|
||||
}
|
||||
|
||||
for (int i = nodes.size()-1; i >= 0; i--) {
|
||||
auto& node = nodes[i];
|
||||
if (!node->isVisible())
|
||||
continue;
|
||||
auto hover = node->getAt(pos, node);
|
||||
auto hover = node->getAt(pos);
|
||||
if (hover != nullptr) {
|
||||
return hover;
|
||||
}
|
||||
}
|
||||
return UINode::getAt(pos, self);
|
||||
return UINode::getAt(pos);
|
||||
}
|
||||
|
||||
void Container::mouseMove(GUI* gui, int x, int y) {
|
||||
@ -172,11 +170,11 @@ void Container::add(const std::shared_ptr<UINode>& node, glm::vec2 pos) {
|
||||
add(node);
|
||||
}
|
||||
|
||||
void Container::remove(const std::shared_ptr<UINode>& selected) {
|
||||
void Container::remove(UINode* selected) {
|
||||
selected->setParent(nullptr);
|
||||
nodes.erase(std::remove_if(nodes.begin(), nodes.end(),
|
||||
[selected](const std::shared_ptr<UINode>& node) {
|
||||
return node == selected;
|
||||
return node.get() == selected;
|
||||
}
|
||||
), nodes.end());
|
||||
refresh();
|
||||
@ -185,7 +183,7 @@ void Container::remove(const std::shared_ptr<UINode>& selected) {
|
||||
void Container::remove(const std::string& id) {
|
||||
for (auto& node : nodes) {
|
||||
if (node->getId() == id) {
|
||||
return remove(node);
|
||||
return remove(node.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,11 +28,11 @@ namespace gui {
|
||||
virtual void act(float delta) override;
|
||||
virtual void drawBackground(const DrawContext& pctx, const Assets& assets);
|
||||
virtual void draw(const DrawContext& pctx, const Assets& assets) override;
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self) override;
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
|
||||
virtual void add(const std::shared_ptr<UINode>& node);
|
||||
virtual void add(const std::shared_ptr<UINode>& node, glm::vec2 pos);
|
||||
virtual void clear();
|
||||
virtual void remove(const std::shared_ptr<UINode>& node);
|
||||
virtual void remove(UINode* node);
|
||||
virtual void remove(const std::string& id);
|
||||
virtual void scrolled(int value) override;
|
||||
virtual void setScrollable(bool flag);
|
||||
|
||||
@ -207,7 +207,7 @@ void SlotView::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
drawItemIcon(batch, stack, item, assets, tint, pos);
|
||||
|
||||
if (stack.getCount() > 1 || stack.getFields() != nullptr) {
|
||||
const auto& font = assets.require<Font>("normal");
|
||||
const auto& font = assets.require<Font>(FONT_DEFAULT);
|
||||
drawItemInfo(batch, stack, item, font, pos);
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ uint LabelCache::getLineByTextIndex(size_t index) const {
|
||||
return lines.size()-1;
|
||||
}
|
||||
|
||||
void LabelCache::update(const std::wstring& text, bool multiline, bool wrap) {
|
||||
void LabelCache::update(std::wstring_view text, bool multiline, bool wrap) {
|
||||
resetFlag = false;
|
||||
lines.clear();
|
||||
lines.push_back(LineScheme {0, false});
|
||||
@ -59,6 +59,27 @@ void LabelCache::update(const std::wstring& text, bool multiline, bool wrap) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (font != nullptr) {
|
||||
int lineHeight = font->getLineHeight();
|
||||
int maxWidth = 0;
|
||||
for (int i = 0; i < lines.size() - 1; i++) {
|
||||
const auto& next = lines[i + 1];
|
||||
const auto& cur = lines[i];
|
||||
maxWidth = std::max(
|
||||
font->calcWidth(
|
||||
text.substr(cur.offset, next.offset - cur.offset)
|
||||
),
|
||||
maxWidth
|
||||
);
|
||||
}
|
||||
maxWidth = std::max(
|
||||
font->calcWidth(
|
||||
text.substr(lines[lines.size() - 1].offset)
|
||||
),
|
||||
maxWidth
|
||||
);
|
||||
multilineWidth = maxWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,8 +110,15 @@ glm::vec2 Label::calcSize() {
|
||||
if (cache.lines.size() > 1) {
|
||||
lineHeight *= lineInterval;
|
||||
}
|
||||
auto view = std::wstring_view(text);
|
||||
if (multiline) {
|
||||
return glm::vec2(
|
||||
cache.multilineWidth,
|
||||
lineHeight * cache.lines.size() + font->getYOffset()
|
||||
);
|
||||
}
|
||||
return glm::vec2 (
|
||||
cache.font->calcWidth(text),
|
||||
cache.font->calcWidth(view),
|
||||
lineHeight * cache.lines.size() + font->getYOffset()
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "UINode.hpp"
|
||||
#include "constants.hpp"
|
||||
|
||||
class Font;
|
||||
struct FontStylesScheme;
|
||||
@ -17,9 +18,10 @@ namespace gui {
|
||||
/// @brief Reset cache flag
|
||||
bool resetFlag = true;
|
||||
size_t wrapWidth = -1;
|
||||
int multilineWidth = 0;
|
||||
|
||||
void prepare(Font* font, size_t wrapWidth);
|
||||
void update(const std::wstring& text, bool multiline, bool wrap);
|
||||
void update(std::wstring_view text, bool multiline, bool wrap);
|
||||
|
||||
size_t getTextLineOffset(size_t line) const;
|
||||
uint getLineByTextIndex(size_t index) const;
|
||||
@ -61,8 +63,8 @@ namespace gui {
|
||||
|
||||
std::unique_ptr<FontStylesScheme> styles;
|
||||
public:
|
||||
Label(const std::string& text, std::string fontName="normal");
|
||||
Label(const std::wstring& text, std::string fontName="normal");
|
||||
Label(const std::string& text, std::string fontName=FONT_DEFAULT);
|
||||
Label(const std::wstring& text, std::string fontName=FONT_DEFAULT);
|
||||
|
||||
virtual ~Label();
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ void Menu::setPage(const std::string &name, bool history) {
|
||||
|
||||
void Menu::setPage(Page page, bool history) {
|
||||
if (current.panel) {
|
||||
Container::remove(current.panel);
|
||||
Container::remove(current.panel.get());
|
||||
if (history && !current.temporal) {
|
||||
pageStack.push(current);
|
||||
}
|
||||
@ -104,7 +104,7 @@ void Menu::clearHistory() {
|
||||
void Menu::reset() {
|
||||
clearHistory();
|
||||
if (current.panel) {
|
||||
Container::remove(current.panel);
|
||||
Container::remove(current.panel.get());
|
||||
current = Page {"", nullptr};
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,9 +5,7 @@
|
||||
using namespace gui;
|
||||
|
||||
Panel::Panel(glm::vec2 size, glm::vec4 padding, float interval)
|
||||
: Container(size),
|
||||
padding(padding),
|
||||
interval(interval)
|
||||
: BasePanel(size, padding, interval, Orientation::vertical)
|
||||
{
|
||||
setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.75f));
|
||||
}
|
||||
@ -31,15 +29,6 @@ int Panel::getMinLength() const {
|
||||
return minLength;
|
||||
}
|
||||
|
||||
void Panel::setPadding(glm::vec4 padding) {
|
||||
this->padding = padding;
|
||||
refresh();
|
||||
}
|
||||
|
||||
glm::vec4 Panel::getPadding() const {
|
||||
return padding;
|
||||
}
|
||||
|
||||
void Panel::cropToContent() {
|
||||
if (maxLength > 0.0f) {
|
||||
setSize(glm::vec2(
|
||||
@ -63,7 +52,7 @@ void Panel::add(const std::shared_ptr<UINode> &node) {
|
||||
fullRefresh();
|
||||
}
|
||||
|
||||
void Panel::remove(const std::shared_ptr<UINode> &node) {
|
||||
void Panel::remove(UINode* node) {
|
||||
Container::remove(node);
|
||||
fullRefresh();
|
||||
}
|
||||
@ -109,11 +98,3 @@ void Panel::refresh() {
|
||||
actualLength = size.y;
|
||||
}
|
||||
}
|
||||
|
||||
void Panel::setOrientation(Orientation orientation) {
|
||||
this->orientation = orientation;
|
||||
}
|
||||
|
||||
Orientation Panel::getOrientation() const {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
@ -1,31 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "commons.hpp"
|
||||
#include "Container.hpp"
|
||||
#include "BasePanel.hpp"
|
||||
|
||||
namespace gui {
|
||||
class Panel : public Container {
|
||||
protected:
|
||||
Orientation orientation = Orientation::vertical;
|
||||
glm::vec4 padding {2.0f};
|
||||
float interval = 2.0f;
|
||||
int minLength = 0;
|
||||
int maxLength = 0;
|
||||
class Panel : public BasePanel {
|
||||
public:
|
||||
Panel(
|
||||
glm::vec2 size,
|
||||
glm::vec4 padding=glm::vec4(2.0f),
|
||||
glm::vec4 padding=glm::vec4(0.0f),
|
||||
float interval=2.0f
|
||||
);
|
||||
virtual ~Panel();
|
||||
|
||||
virtual void cropToContent();
|
||||
|
||||
virtual void setOrientation(Orientation orientation);
|
||||
Orientation getOrientation() const;
|
||||
|
||||
virtual void add(const std::shared_ptr<UINode>& node) override;
|
||||
virtual void remove(const std::shared_ptr<UINode>& node) override;
|
||||
virtual void remove(UINode* node) override;
|
||||
|
||||
virtual void refresh() override;
|
||||
virtual void fullRefresh() override;
|
||||
@ -35,8 +26,8 @@ namespace gui {
|
||||
|
||||
virtual void setMinLength(int value);
|
||||
int getMinLength() const;
|
||||
|
||||
virtual void setPadding(glm::vec4 padding);
|
||||
glm::vec4 getPadding() const;
|
||||
protected:
|
||||
int minLength = 0;
|
||||
int maxLength = 0;
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "graphics/core/DrawContext.hpp"
|
||||
#include "assets/Assets.hpp"
|
||||
#include "util/stringutil.hpp"
|
||||
#include "constants.hpp"
|
||||
|
||||
using namespace gui;
|
||||
|
||||
@ -37,7 +38,7 @@ void Plotter::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
}
|
||||
|
||||
int current_point = static_cast<int>(points[index % dmwidth]);
|
||||
auto font = assets.get<Font>("normal");
|
||||
auto font = assets.get<Font>(FONT_DEFAULT);
|
||||
for (int y = 0; y < dmheight; y += labelsInterval) {
|
||||
std::wstring string;
|
||||
if (current_point/16 == y/labelsInterval) {
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
#include "typedefs.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
class Assets;
|
||||
class DrawContext;
|
||||
|
||||
74
src/graphics/ui/elements/SplitBox.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
#include "SplitBox.hpp"
|
||||
|
||||
using namespace gui;
|
||||
|
||||
SplitBox::SplitBox(const glm::vec2& size, float splitPos, Orientation orientation)
|
||||
: BasePanel(size, glm::vec4(), 4.0f, orientation), splitPos(splitPos) {
|
||||
setCursor(
|
||||
orientation == Orientation::vertical ? CursorShape::NS_RESIZE
|
||||
: CursorShape::EW_RESIZE
|
||||
);
|
||||
}
|
||||
|
||||
void SplitBox::mouseMove(GUI*, int x, int y) {
|
||||
auto pos = calcPos();
|
||||
auto size = getSize();
|
||||
|
||||
glm::ivec2 cursor(x - pos.x, y - pos.y);
|
||||
int axis = orientation == Orientation::vertical;
|
||||
|
||||
int v = cursor[axis];
|
||||
v = std::max(std::min(static_cast<int>(size[axis]) - 10, v), 10);
|
||||
float t = v / size[axis];
|
||||
splitPos = t;
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SplitBox::refresh() {
|
||||
Container::refresh();
|
||||
|
||||
if (nodes.empty()) {
|
||||
return;
|
||||
}
|
||||
glm::vec2 size = getSize();
|
||||
if (nodes.size() == 1) {
|
||||
auto node = nodes.at(0);
|
||||
node->setPos(glm::vec2());
|
||||
node->setSize(size);
|
||||
return;
|
||||
}
|
||||
auto nodeA = nodes.at(0);
|
||||
auto nodeB = nodes.at(1);
|
||||
|
||||
float sepRadius = interval / 2.0f;
|
||||
|
||||
nodeA->setPos(glm::vec2(padding));
|
||||
|
||||
const auto& p = padding;
|
||||
if (orientation == Orientation::vertical) {
|
||||
float splitPos = this->splitPos * size.y;
|
||||
nodeA->setSize({size.x-p.x-p.z, splitPos - sepRadius - p.y});
|
||||
nodeB->setSize({size.x-p.x-p.z, size.y - splitPos - sepRadius - p.w});
|
||||
nodeB->setPos({p.x, splitPos + sepRadius});
|
||||
} else {
|
||||
float splitPos = this->splitPos * size.x;
|
||||
nodeA->setSize({splitPos - sepRadius - p.x, size.y - p.y - p.w});
|
||||
nodeB->setSize({size.x - splitPos - sepRadius - p.z, size.y - p.y - p.w});
|
||||
nodeB->setPos({splitPos + sepRadius, p.y});
|
||||
}
|
||||
}
|
||||
|
||||
void SplitBox::doubleClick(GUI*, int x, int y) {
|
||||
if (nodes.size() < 2) {
|
||||
return;
|
||||
}
|
||||
std::swap(nodes[0], nodes[1]);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SplitBox::fullRefresh() {
|
||||
refresh();
|
||||
reposition();
|
||||
Container::fullRefresh();
|
||||
}
|
||||
17
src/graphics/ui/elements/SplitBox.hpp
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "BasePanel.hpp"
|
||||
|
||||
namespace gui {
|
||||
class SplitBox : public BasePanel {
|
||||
public:
|
||||
SplitBox(const glm::vec2& size, float splitPos, Orientation orientation);
|
||||
|
||||
virtual void mouseMove(GUI*, int x, int y) override;
|
||||
virtual void refresh() override;
|
||||
virtual void fullRefresh() override;
|
||||
virtual void doubleClick(GUI*, int x, int y) override;
|
||||
private:
|
||||
float splitPos;
|
||||
};
|
||||
}
|
||||
@ -13,18 +13,185 @@
|
||||
#include "util/stringutil.hpp"
|
||||
#include "window/Events.hpp"
|
||||
#include "window/Window.hpp"
|
||||
#include "devtools/actions.hpp"
|
||||
#include "../markdown.hpp"
|
||||
|
||||
using namespace gui;
|
||||
|
||||
inline constexpr int LINE_NUMBERS_PANE_WIDTH = 40;
|
||||
|
||||
TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
|
||||
: Container(glm::vec2(200,32)),
|
||||
padding(padding),
|
||||
input(L""),
|
||||
placeholder(std::move(placeholder))
|
||||
{
|
||||
class InputAction : public Action {
|
||||
std::weak_ptr<TextBox> textbox;
|
||||
size_t position;
|
||||
std::wstring string;
|
||||
public:
|
||||
InputAction(
|
||||
std::weak_ptr<TextBox> textbox, size_t position, std::wstring string
|
||||
)
|
||||
: textbox(std::move(textbox)),
|
||||
position(position),
|
||||
string(std::move(string)) {
|
||||
}
|
||||
void apply() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(position, position);
|
||||
box->paste(string);
|
||||
}
|
||||
}
|
||||
|
||||
void revert() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(position, position);
|
||||
box->erase(position, string.length());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class SelectionAction : public Action {
|
||||
std::weak_ptr<TextBox> textbox;
|
||||
size_t start;
|
||||
size_t end;
|
||||
public:
|
||||
SelectionAction(std::weak_ptr<TextBox> textbox, size_t start, size_t end)
|
||||
: textbox(std::move(textbox)), start(start), end(end) {}
|
||||
|
||||
void apply() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
void revert() override {
|
||||
if (auto box = textbox.lock()) {
|
||||
box->select(0, 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace gui {
|
||||
/// @brief Accumulates small changes into words for InputAction creation
|
||||
class TextBoxHistorian {
|
||||
public:
|
||||
TextBoxHistorian(TextBox& textBox, ActionsHistory& history)
|
||||
: textBox(textBox), history(history) {
|
||||
}
|
||||
|
||||
void onPaste(size_t pos, std::wstring_view text) {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
if (erasing) {
|
||||
sync();
|
||||
}
|
||||
if (this->pos == static_cast<size_t>(-1)) {
|
||||
this->pos = pos;
|
||||
}
|
||||
if (this->pos + length != pos || text == L" " || text == L"\n") {
|
||||
sync();
|
||||
this->pos = pos;
|
||||
}
|
||||
ss << text;
|
||||
length += text.length();
|
||||
}
|
||||
|
||||
void onErase(size_t pos, std::wstring_view text, bool selection=false) {
|
||||
if (locked) {
|
||||
return;
|
||||
}
|
||||
if (!erasing) {
|
||||
sync();
|
||||
erasing = true;
|
||||
}
|
||||
if (selection) {
|
||||
history.store(
|
||||
std::make_unique<SelectionAction>(
|
||||
getTextBoxWeakptr(),
|
||||
textBox.getSelectionStart(),
|
||||
textBox.getSelectionEnd()
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
if (this->pos == static_cast<size_t>(-1)) {
|
||||
this->pos = pos;
|
||||
} else if (this->pos - text.length() != pos) {
|
||||
sync();
|
||||
erasing = true;
|
||||
this->pos = pos;
|
||||
}
|
||||
if (text == L" " || text == L"\n") {
|
||||
sync();
|
||||
erasing = true;
|
||||
this->pos = pos;
|
||||
}
|
||||
auto str = ss.str();
|
||||
ss.seekp(0);
|
||||
ss << text << str;
|
||||
|
||||
this->pos = pos;
|
||||
length += text.length();
|
||||
}
|
||||
|
||||
/// @brief Flush buffer and push all changes to the ActionsHistory
|
||||
void sync() {
|
||||
auto string = ss.str();
|
||||
if (string.empty()) {
|
||||
return;
|
||||
}
|
||||
auto action =
|
||||
std::make_unique<InputAction>(getTextBoxWeakptr(), pos, string);
|
||||
history.store(std::move(action), erasing);
|
||||
reset();
|
||||
}
|
||||
|
||||
void undo() {
|
||||
sync();
|
||||
locked = true;
|
||||
history.undo();
|
||||
locked = false;
|
||||
}
|
||||
|
||||
void redo() {
|
||||
sync();
|
||||
locked = true;
|
||||
history.redo();
|
||||
locked = false;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
pos = -1;
|
||||
length = 0;
|
||||
erasing = false;
|
||||
ss = {};
|
||||
}
|
||||
|
||||
bool isSynced() const {
|
||||
return length == 0;
|
||||
}
|
||||
private:
|
||||
TextBox& textBox;
|
||||
ActionsHistory& history;
|
||||
std::wstringstream ss;
|
||||
size_t pos = -1;
|
||||
size_t length = 0;
|
||||
bool erasing = false;
|
||||
bool locked = false;
|
||||
|
||||
std::weak_ptr<TextBox> getTextBoxWeakptr() {
|
||||
return std::weak_ptr<TextBox>(std::dynamic_pointer_cast<TextBox>(
|
||||
textBox.shared_from_this()
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
|
||||
: Container(glm::vec2(200, 32)),
|
||||
history(std::make_shared<ActionsHistory>()),
|
||||
historian(std::make_unique<TextBoxHistorian>(*this, *history)),
|
||||
padding(padding),
|
||||
input(L""),
|
||||
placeholder(std::move(placeholder)) {
|
||||
setCursor(CursorShape::TEXT);
|
||||
setOnUpPressed(nullptr);
|
||||
setOnDownPressed(nullptr);
|
||||
@ -49,6 +216,8 @@ TextBox::TextBox(std::wstring placeholder, glm::vec4 padding)
|
||||
scrollStep = 0;
|
||||
}
|
||||
|
||||
TextBox::~TextBox() = default;
|
||||
|
||||
void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
Container::draw(pctx, assets);
|
||||
|
||||
@ -71,6 +240,7 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
auto batch = pctx.getBatch2D();
|
||||
batch->texture(nullptr);
|
||||
batch->setColor(glm::vec4(1.0f));
|
||||
|
||||
if (editable && int((Window::time() - caretLastMove) * 2) % 2 == 0) {
|
||||
uint line = rawTextCache.getLineByTextIndex(caret);
|
||||
uint lcaret = caret - rawTextCache.getTextLineOffset(line);
|
||||
@ -138,7 +308,6 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
|
||||
}
|
||||
do {
|
||||
int lineY = label->getLineYOffset(line);
|
||||
int lineHeight = font->getLineHeight() * label->getLineInterval();
|
||||
|
||||
batch->setColor(glm::vec4(1, 1, 1, 0.05f));
|
||||
if (showLineNumbers) {
|
||||
@ -260,18 +429,22 @@ void TextBox::refreshLabel() {
|
||||
|
||||
/// @brief Insert text at the caret. Also selected text will be erased
|
||||
/// @param text Inserting text
|
||||
void TextBox::paste(const std::wstring& text) {
|
||||
void TextBox::paste(const std::wstring& text, bool history) {
|
||||
eraseSelected();
|
||||
auto inputText = text;
|
||||
inputText.erase(
|
||||
std::remove(inputText.begin(), inputText.end(), '\r'), inputText.end()
|
||||
);
|
||||
historian->onPaste(caret, inputText);
|
||||
if (caret >= input.length()) {
|
||||
input += text;
|
||||
input += inputText;
|
||||
} else {
|
||||
auto left = input.substr(0, caret);
|
||||
auto right = input.substr(caret);
|
||||
input = left + text + right;
|
||||
input = left + inputText + right;
|
||||
}
|
||||
input.erase(std::remove(input.begin(), input.end(), '\r'), input.end());
|
||||
refreshLabel();
|
||||
setCaret(caret + text.length());
|
||||
setCaret(caret + inputText.length());
|
||||
if (validate()) {
|
||||
onInput();
|
||||
}
|
||||
@ -296,6 +469,11 @@ bool TextBox::eraseSelected() {
|
||||
if (selectionStart == selectionEnd) {
|
||||
return false;
|
||||
}
|
||||
historian->onErase(
|
||||
selectionStart,
|
||||
input.substr(selectionStart, selectionEnd - selectionStart),
|
||||
true
|
||||
);
|
||||
erase(selectionStart, selectionEnd-selectionStart);
|
||||
resetSelection();
|
||||
onInput();
|
||||
@ -336,7 +514,9 @@ void TextBox::setTextOffset(uint x) {
|
||||
|
||||
void TextBox::typed(unsigned int codepoint) {
|
||||
if (editable) {
|
||||
paste(std::wstring({(wchar_t)codepoint}));
|
||||
// Combine deleting selected text and inserting a symbol
|
||||
auto combination = history->beginCombination();
|
||||
paste(std::wstring({static_cast<wchar_t>(codepoint)}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,6 +563,23 @@ bool TextBox::isEditable() const {
|
||||
return editable;
|
||||
}
|
||||
|
||||
bool TextBox::isEdited() const {
|
||||
return history->size() != editedHistorySize || !historian->isSynced();
|
||||
}
|
||||
|
||||
void TextBox::setUnedited() {
|
||||
historian->sync();
|
||||
editedHistorySize = history->size();
|
||||
}
|
||||
|
||||
size_t TextBox::getSelectionStart() const {
|
||||
return selectionStart;
|
||||
}
|
||||
|
||||
size_t TextBox::getSelectionEnd() const {
|
||||
return selectionEnd;
|
||||
}
|
||||
|
||||
void TextBox::setOnEditStart(runnable oneditstart) {
|
||||
onEditStart = oneditstart;
|
||||
}
|
||||
@ -404,6 +601,12 @@ void TextBox::onFocus(GUI* gui) {
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::reposition() {
|
||||
auto size = getSize();
|
||||
UINode::reposition();
|
||||
refreshLabel();
|
||||
}
|
||||
|
||||
void TextBox::refresh() {
|
||||
Container::refresh();
|
||||
label->setSize(size-glm::vec2(padding.z+padding.x, padding.w+padding.y));
|
||||
@ -609,6 +812,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) {
|
||||
if (caret > input.length()) {
|
||||
caret = input.length();
|
||||
}
|
||||
historian->onErase(caret - 1, input.substr(caret - 1, 1));
|
||||
input = input.substr(0, caret-1) + input.substr(caret);
|
||||
setCaret(caret-1);
|
||||
if (validate()) {
|
||||
@ -617,6 +821,7 @@ void TextBox::performEditingKeyboardEvents(keycode key) {
|
||||
}
|
||||
} else if (key == keycode::DELETE) {
|
||||
if (!eraseSelected() && caret < input.length()) {
|
||||
historian->onErase(caret, input.substr(caret, 1));
|
||||
input = input.substr(0, caret) + input.substr(caret + 1);
|
||||
if (validate()) {
|
||||
onInput();
|
||||
@ -648,7 +853,12 @@ void TextBox::keyPressed(keycode key) {
|
||||
if (editable) {
|
||||
performEditingKeyboardEvents(key);
|
||||
}
|
||||
if (Events::pressed(keycode::LEFT_CONTROL)) {
|
||||
if (Events::pressed(keycode::LEFT_CONTROL) && key != keycode::LEFT_CONTROL) {
|
||||
if (controlCombinationsHandler) {
|
||||
if (controlCombinationsHandler(static_cast<int>(key))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Copy selected text to clipboard
|
||||
if (key == keycode::C || key == keycode::X) {
|
||||
std::string text = util::wstr2str_utf8(getSelection());
|
||||
@ -663,7 +873,11 @@ void TextBox::keyPressed(keycode key) {
|
||||
if (key == keycode::V && editable) {
|
||||
const char* text = Window::getClipboardText();
|
||||
if (text) {
|
||||
historian->sync(); // flush buffer before combination
|
||||
// Combine deleting selected text and pasing a clipboard content
|
||||
auto combination = history->beginCombination();
|
||||
paste(util::str2wstr_utf8(text));
|
||||
historian->sync();
|
||||
}
|
||||
}
|
||||
// Select/deselect all
|
||||
@ -674,6 +888,14 @@ void TextBox::keyPressed(keycode key) {
|
||||
resetSelection();
|
||||
}
|
||||
}
|
||||
if (key == keycode::Z) {
|
||||
historian->undo();
|
||||
refreshSyntax();
|
||||
}
|
||||
if (key == keycode::Y) {
|
||||
historian->redo();
|
||||
refreshSyntax();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -698,10 +920,8 @@ size_t TextBox::getLinePos(uint line) const {
|
||||
return label->getTextLineOffset(line);
|
||||
}
|
||||
|
||||
std::shared_ptr<UINode> TextBox::getAt(
|
||||
const glm::vec2& pos, const std::shared_ptr<UINode>& self
|
||||
) {
|
||||
return UINode::getAt(pos, self);
|
||||
std::shared_ptr<UINode> TextBox::getAt(const glm::vec2& pos) {
|
||||
return UINode::getAt(pos);
|
||||
}
|
||||
|
||||
void TextBox::setOnUpPressed(const runnable &callback) {
|
||||
@ -752,6 +972,10 @@ void TextBox::setTextValidator(wstringchecker validator) {
|
||||
this->validator = std::move(validator);
|
||||
}
|
||||
|
||||
void TextBox::setOnControlCombination(key_handler handler) {
|
||||
this->controlCombinationsHandler = std::move(handler);
|
||||
}
|
||||
|
||||
void TextBox::setFocusedColor(glm::vec4 color) {
|
||||
this->focusedColor = color;
|
||||
}
|
||||
@ -786,6 +1010,9 @@ const std::wstring& TextBox::getText() const {
|
||||
void TextBox::setText(const std::wstring& value) {
|
||||
this->input = value;
|
||||
input.erase(std::remove(input.begin(), input.end(), '\r'), input.end());
|
||||
historian->reset();
|
||||
history->clear();
|
||||
editedHistorySize = 0;
|
||||
refreshSyntax();
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,15 @@
|
||||
#include "Label.hpp"
|
||||
|
||||
class Font;
|
||||
class ActionsHistory;
|
||||
|
||||
namespace gui {
|
||||
class TextBoxHistorian;
|
||||
class TextBox : public Container {
|
||||
LabelCache rawTextCache;
|
||||
std::shared_ptr<ActionsHistory> history;
|
||||
std::unique_ptr<TextBoxHistorian> historian;
|
||||
int editedHistorySize = 0;
|
||||
protected:
|
||||
glm::vec4 focusedColor {0.0f, 0.0f, 0.0f, 1.0f};
|
||||
glm::vec4 invalidColor {0.1f, 0.05f, 0.03f, 1.0f};
|
||||
@ -29,6 +34,7 @@ namespace gui {
|
||||
wstringconsumer subconsumer = nullptr;
|
||||
/// @brief Text validator returning boolean value
|
||||
wstringchecker validator = nullptr;
|
||||
key_handler controlCombinationsHandler = nullptr;
|
||||
/// @brief Function called on focus
|
||||
runnable onEditStart = nullptr;
|
||||
/// @brief Function called on up arrow pressed
|
||||
@ -68,7 +74,6 @@ namespace gui {
|
||||
|
||||
int calcIndexAt(int x, int y) const;
|
||||
void setTextOffset(uint x);
|
||||
void erase(size_t start, size_t length);
|
||||
bool eraseSelected();
|
||||
void resetSelection();
|
||||
void extendSelection(int index);
|
||||
@ -93,8 +98,11 @@ namespace gui {
|
||||
std::wstring placeholder,
|
||||
glm::vec4 padding=glm::vec4(4.0f)
|
||||
);
|
||||
|
||||
virtual ~TextBox();
|
||||
|
||||
void paste(const std::wstring& text);
|
||||
void paste(const std::wstring& text, bool history=true);
|
||||
void erase(size_t start, size_t length);
|
||||
|
||||
virtual void setTextSupplier(wstringsupplier supplier);
|
||||
|
||||
@ -111,6 +119,8 @@ namespace gui {
|
||||
/// @param validator std::wstring consumer returning boolean
|
||||
virtual void setTextValidator(wstringchecker validator);
|
||||
|
||||
virtual void setOnControlCombination(key_handler handler);
|
||||
|
||||
virtual void setFocusedColor(glm::vec4 color);
|
||||
virtual glm::vec4 getFocusedColor() const;
|
||||
|
||||
@ -198,9 +208,15 @@ namespace gui {
|
||||
/// @brief Check if text editing feature is enabled
|
||||
virtual bool isEditable() const;
|
||||
|
||||
virtual bool isEdited() const;
|
||||
virtual void setUnedited();
|
||||
|
||||
virtual void setPadding(glm::vec4 padding);
|
||||
glm::vec4 getPadding() const;
|
||||
|
||||
size_t getSelectionStart() const;
|
||||
size_t getSelectionEnd() const;
|
||||
|
||||
/// @brief Set runnable called on textbox focus
|
||||
virtual void setOnEditStart(runnable oneditstart);
|
||||
|
||||
@ -210,6 +226,7 @@ namespace gui {
|
||||
virtual void setShowLineNumbers(bool flag);
|
||||
virtual bool isShowLineNumbers() const;
|
||||
|
||||
virtual void reposition() override;
|
||||
virtual void onFocus(GUI*) override;
|
||||
virtual void refresh() override;
|
||||
virtual void doubleClick(GUI*, int x, int y) override;
|
||||
@ -220,9 +237,7 @@ namespace gui {
|
||||
virtual void drawBackground(const DrawContext& pctx, const Assets& assets) override;
|
||||
virtual void typed(unsigned int codepoint) override;
|
||||
virtual void keyPressed(keycode key) override;
|
||||
virtual std::shared_ptr<UINode> getAt(
|
||||
const glm::vec2& pos, const std::shared_ptr<UINode>& self
|
||||
) override;
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos) override;
|
||||
virtual void setOnUpPressed(const runnable &callback);
|
||||
virtual void setOnDownPressed(const runnable &callback);
|
||||
|
||||
|
||||
@ -111,11 +111,11 @@ bool UINode::isInside(glm::vec2 point) {
|
||||
point.x < pos.x + size.x && point.y < pos.y + size.y);
|
||||
}
|
||||
|
||||
std::shared_ptr<UINode> UINode::getAt(const glm::vec2& point, const std::shared_ptr<UINode>& self) {
|
||||
std::shared_ptr<UINode> UINode::getAt(const glm::vec2& point) {
|
||||
if (!isInteractive() || !enabled) {
|
||||
return nullptr;
|
||||
}
|
||||
return isInside(point) ? self : nullptr;
|
||||
return isInside(point) ? shared_from_this() : nullptr;
|
||||
}
|
||||
|
||||
bool UINode::isInteractive() const {
|
||||
@ -266,7 +266,7 @@ void UINode::moveInto(
|
||||
) {
|
||||
auto parent = node->getParent();
|
||||
if (auto container = dynamic_cast<Container*>(parent)) {
|
||||
container->remove(node);
|
||||
container->remove(node.get());
|
||||
}
|
||||
if (parent) {
|
||||
parent->scrolled(0);
|
||||
@ -301,9 +301,13 @@ const std::string& UINode::getId() const {
|
||||
void UINode::reposition() {
|
||||
if (sizefunc) {
|
||||
auto newSize = sizefunc();
|
||||
auto defsize = newSize;
|
||||
if (parent) {
|
||||
defsize = parent->getSize();
|
||||
}
|
||||
setSize(
|
||||
{newSize.x < 0 ? size.x : newSize.x,
|
||||
newSize.y < 0 ? size.y : newSize.y}
|
||||
{newSize.x < 0 ? defsize.x + (newSize.x + 1) : newSize.x,
|
||||
newSize.y < 0 ? defsize.y + (newSize.y + 1) : newSize.y}
|
||||
);
|
||||
}
|
||||
if (positionfunc) {
|
||||
|
||||
@ -63,7 +63,7 @@ namespace gui {
|
||||
};
|
||||
|
||||
/// @brief Base abstract class for all UI elements
|
||||
class UINode {
|
||||
class UINode : public std::enable_shared_from_this<UINode> {
|
||||
/// @brief element identifier used for direct access in UiDocument
|
||||
std::string id = "";
|
||||
/// @brief element enabled state
|
||||
@ -195,7 +195,7 @@ namespace gui {
|
||||
/// @param pos cursor screen position
|
||||
/// @param self shared pointer to element
|
||||
/// @return self, sub-element or nullptr if element is not interractive
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos, const std::shared_ptr<UINode>& self);
|
||||
virtual std::shared_ptr<UINode> getAt(const glm::vec2& pos);
|
||||
|
||||
/// @brief Check if element is opaque for cursor
|
||||
virtual bool isInteractive() const;
|
||||
@ -250,7 +250,7 @@ namespace gui {
|
||||
const std::string& getId() const;
|
||||
|
||||
/// @brief Fetch pos from positionfunc if assigned
|
||||
void reposition();
|
||||
virtual void reposition();
|
||||
|
||||
virtual void setGravity(Gravity gravity);
|
||||
|
||||
|
||||
@ -91,7 +91,11 @@ void guiutil::confirm(
|
||||
if (yestext.empty()) yestext = langs::get(L"Yes");
|
||||
if (notext.empty()) notext = langs::get(L"No");
|
||||
|
||||
auto container = std::make_shared<Container>(glm::vec2(5000, 5000));
|
||||
container->setColor(glm::vec4(0.05f, 0.05f, 0.05f, 0.7f));
|
||||
auto panel = std::make_shared<Panel>(glm::vec2(600, 200), glm::vec4(8.0f), 8.0f);
|
||||
panel->setGravity(Gravity::center_center);
|
||||
container->add(panel);
|
||||
panel->setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.5f));
|
||||
panel->add(std::make_shared<Label>(text));
|
||||
auto subpanel = std::make_shared<Panel>(glm::vec2(600, 53));
|
||||
@ -103,8 +107,8 @@ void guiutil::confirm(
|
||||
menu->removePage("<confirm>");
|
||||
if (on_confirm) {
|
||||
on_confirm();
|
||||
} else {
|
||||
menu->back();
|
||||
} else if (!menu->back()) {
|
||||
menu->reset();
|
||||
}
|
||||
};
|
||||
|
||||
@ -112,8 +116,8 @@ void guiutil::confirm(
|
||||
menu->removePage("<confirm>");
|
||||
if (on_deny) {
|
||||
on_deny();
|
||||
} else {
|
||||
menu->back();
|
||||
} else if (!menu->back()) {
|
||||
menu->reset();
|
||||
}
|
||||
};
|
||||
|
||||
@ -136,7 +140,7 @@ void guiutil::confirm(
|
||||
}));
|
||||
|
||||
panel->refresh();
|
||||
menu->addPage("<confirm>", panel, true);
|
||||
menu->addPage("<confirm>", container, true);
|
||||
menu->setPage("<confirm>");
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "elements/Canvas.hpp"
|
||||
#include "elements/CheckBox.hpp"
|
||||
#include "elements/TextBox.hpp"
|
||||
#include "elements/SplitBox.hpp"
|
||||
#include "elements/TrackBar.hpp"
|
||||
#include "elements/InputBindBox.hpp"
|
||||
#include "elements/InventoryView.hpp"
|
||||
@ -157,7 +158,13 @@ static void read_uinode(
|
||||
}
|
||||
|
||||
if (element.has("tooltip")) {
|
||||
node.setTooltip(util::str2wstr_utf8(element.attr("tooltip").getText()));
|
||||
auto tooltip = util::str2wstr_utf8(element.attr("tooltip").getText());
|
||||
if (!tooltip.empty() && tooltip[0] == '@') {
|
||||
tooltip = langs::get(
|
||||
tooltip.substr(1), util::str2wstr_utf8(reader.getContext())
|
||||
);
|
||||
}
|
||||
node.setTooltip(tooltip);
|
||||
}
|
||||
if (element.has("tooltip-delay")) {
|
||||
node.setTooltipDelay(element.attr("tooltip-delay").asFloat());
|
||||
@ -206,11 +213,10 @@ void UiXmlReader::readUINode(
|
||||
read_uinode(reader, element, node);
|
||||
}
|
||||
|
||||
static void read_panel_impl(
|
||||
static void read_base_panel_impl(
|
||||
UiXmlReader& reader,
|
||||
const xml::xmlelement& element,
|
||||
Panel& panel,
|
||||
bool subnodes = true
|
||||
BasePanel& panel
|
||||
) {
|
||||
read_uinode(reader, element, panel);
|
||||
|
||||
@ -223,6 +229,22 @@ static void read_panel_impl(
|
||||
size.y + padding.y + padding.w
|
||||
));
|
||||
}
|
||||
if (element.has("orientation")) {
|
||||
auto &oname = element.attr("orientation").getText();
|
||||
if (oname == "horizontal") {
|
||||
panel.setOrientation(Orientation::horizontal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void read_panel_impl(
|
||||
UiXmlReader& reader,
|
||||
const xml::xmlelement& element,
|
||||
Panel& panel,
|
||||
bool subnodes = true
|
||||
) {
|
||||
read_base_panel_impl(reader, element, panel);
|
||||
|
||||
if (element.has("size")) {
|
||||
panel.setResizing(false);
|
||||
}
|
||||
@ -232,12 +254,6 @@ static void read_panel_impl(
|
||||
if (element.has("min-length")) {
|
||||
panel.setMinLength(element.attr("min-length").asInt());
|
||||
}
|
||||
if (element.has("orientation")) {
|
||||
auto &oname = element.attr("orientation").getText();
|
||||
if (oname == "horizontal") {
|
||||
panel.setOrientation(Orientation::horizontal);
|
||||
}
|
||||
}
|
||||
if (subnodes) {
|
||||
for (auto& sub : element.getElements()) {
|
||||
if (sub->isText())
|
||||
@ -313,6 +329,28 @@ static std::shared_ptr<UINode> read_container(
|
||||
return container;
|
||||
}
|
||||
|
||||
static std::shared_ptr<UINode> read_split_box(
|
||||
UiXmlReader& reader, const xml::xmlelement& element
|
||||
) {
|
||||
float splitPos = element.attr("split-pos", "0.5").asFloat();
|
||||
Orientation orientation =
|
||||
element.attr("orientation", "vertical").getText() == "horizontal"
|
||||
? Orientation::horizontal
|
||||
: Orientation::vertical;
|
||||
auto splitBox =
|
||||
std::make_shared<SplitBox>(glm::vec2(), splitPos, orientation);
|
||||
read_base_panel_impl(reader, element, *splitBox);
|
||||
for (auto& sub : element.getElements()) {
|
||||
if (sub->isText())
|
||||
continue;
|
||||
auto subnode = reader.readUINode(*sub);
|
||||
if (subnode) {
|
||||
splitBox->add(subnode);
|
||||
}
|
||||
}
|
||||
return splitBox;
|
||||
}
|
||||
|
||||
static std::shared_ptr<UINode> read_panel(
|
||||
UiXmlReader& reader, const xml::xmlelement& element
|
||||
) {
|
||||
@ -455,6 +493,13 @@ static std::shared_ptr<UINode> read_text_box(
|
||||
reader.getFilename()
|
||||
));
|
||||
}
|
||||
if (element.has("oncontrolkey")) {
|
||||
textbox->setOnControlCombination(scripting::create_key_handler(
|
||||
reader.getEnvironment(),
|
||||
element.attr("oncontrolkey").getText(),
|
||||
reader.getFilename()
|
||||
));
|
||||
}
|
||||
if (auto onUpPressed = create_runnable(reader, element, "onup")) {
|
||||
textbox->setOnUpPressed(onUpPressed);
|
||||
}
|
||||
@ -677,6 +722,7 @@ UiXmlReader::UiXmlReader(const scriptenv& env) : env(env) {
|
||||
add("button", read_button);
|
||||
add("textbox", read_text_box);
|
||||
add("pagebox", read_page_box);
|
||||
add("splitbox", read_split_box);
|
||||
add("checkbox", read_check_box);
|
||||
add("trackbar", read_track_bar);
|
||||
add("container", read_container);
|
||||
|
||||
@ -23,10 +23,6 @@ static inline void emit_md(
|
||||
|
||||
template <typename CharT>
|
||||
static glm::vec4 parse_color(const std::basic_string_view<CharT>& color_code) {
|
||||
if (color_code.size() != 8 || color_code[0] != '#') {
|
||||
return glm::vec4(1, 1, 1, 1); // default to white
|
||||
}
|
||||
|
||||
auto hex_to_float = [](char high, char low) {
|
||||
int high_val = hexchar2int(high);
|
||||
int low_val = hexchar2int(low);
|
||||
@ -36,11 +32,18 @@ static glm::vec4 parse_color(const std::basic_string_view<CharT>& color_code) {
|
||||
return (high_val * 16 + low_val) / 255.0f;
|
||||
};
|
||||
|
||||
if (color_code[0] != '#') {
|
||||
return glm::vec4(1, 1, 1, 1);
|
||||
}
|
||||
|
||||
if (color_code.size() < 8) {
|
||||
return glm::vec4(1, 1, 1, 1);
|
||||
}
|
||||
return glm::vec4(
|
||||
hex_to_float(color_code[1], color_code[2]),
|
||||
hex_to_float(color_code[3], color_code[4]),
|
||||
hex_to_float(color_code[5], color_code[6]),
|
||||
1
|
||||
color_code.size() == 10 ? hex_to_float(color_code[7], color_code[8]) : 1
|
||||
);
|
||||
}
|
||||
|
||||
@ -99,18 +102,24 @@ Result<CharT> process_markdown(
|
||||
pos++;
|
||||
continue;
|
||||
case '[':
|
||||
if (pos + 9 < source.size() && source[pos + 1] == '#' &&
|
||||
source[pos + 8] == ']') {
|
||||
if (!eraseMarkdown) {
|
||||
emit_md(source[pos - 1], styles, ss);
|
||||
if (source[pos + 1] == '#') {
|
||||
int closingPos = -1;
|
||||
if (pos + 8 < source.size() && source[pos + 8] == ']') {
|
||||
closingPos = 8;
|
||||
} else if (pos + 10 < source.size() && source[pos + 10] == ']') {
|
||||
closingPos = 10;
|
||||
}
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
|
||||
emit(source[pos + i], styles, ss);
|
||||
if (closingPos != -1) {
|
||||
if (!eraseMarkdown) {
|
||||
emit_md(source[pos - 1], styles, ss);
|
||||
}
|
||||
int length = closingPos + 2;
|
||||
for (int i = 0; i < length; ++i) {
|
||||
emit(source[pos + i], styles, ss);
|
||||
}
|
||||
pos += length;
|
||||
continue;
|
||||
}
|
||||
|
||||
pos += 10;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pos--;
|
||||
@ -125,6 +134,16 @@ Result<CharT> process_markdown(
|
||||
}
|
||||
pos += 9; // Skip past the color code
|
||||
continue;
|
||||
} else if (first == '[' && pos + 11 <= source.size() && source[pos + 1] == '#' && source[pos + 10] == ']') {
|
||||
std::basic_string_view<CharT> color_code = source.substr(pos + 1, 10);
|
||||
apply_color(color_code, styles, style);
|
||||
if (!eraseMarkdown) {
|
||||
for (int i = 0; i < 11; ++i) {
|
||||
emit_md(source[pos + i], styles, ss);
|
||||
}
|
||||
}
|
||||
pos += 11; // Skip past the color code
|
||||
continue;
|
||||
} else if (first == '*') {
|
||||
if (pos + 1 < source.size() && source[pos + 1] == '*') {
|
||||
pos++;
|
||||
|
||||
@ -150,11 +150,45 @@ void EnginePaths::setCurrentWorldFolder(io::path folder) {
|
||||
io::create_subdevice("world", "user", currentWorldFolder);
|
||||
}
|
||||
|
||||
#include <chrono>
|
||||
#include "maths/util.hpp"
|
||||
|
||||
std::string EnginePaths::createWriteablePackDevice(const std::string& name) {
|
||||
const auto& found = writeablePacks.find(name);
|
||||
if (found != writeablePacks.end()) {
|
||||
return found->second;
|
||||
}
|
||||
io::path folder;
|
||||
for (const auto& pack : *contentPacks) {
|
||||
if (pack.id == name) {
|
||||
folder = pack.folder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (folder.emptyOrInvalid()) {
|
||||
throw std::runtime_error("pack not found");
|
||||
}
|
||||
|
||||
auto now = std::chrono::high_resolution_clock::now();
|
||||
auto seed = now.time_since_epoch().count();
|
||||
|
||||
util::PseudoRandom random(seed); // fixme: replace with safe random
|
||||
auto number = random.rand64();
|
||||
auto entryPoint = std::string("W.") + util::base64_urlsafe_encode(reinterpret_cast<ubyte*>(&number), 6);
|
||||
|
||||
io::create_subdevice(entryPoint, folder.entryPoint(), folder.pathPart());
|
||||
writeablePacks[name] = entryPoint;
|
||||
return entryPoint;
|
||||
}
|
||||
|
||||
void EnginePaths::setContentPacks(std::vector<ContentPack>* contentPacks) {
|
||||
// Remove previous content entry-points
|
||||
for (const auto& id : contentEntryPoints) {
|
||||
io::remove_device(id);
|
||||
}
|
||||
for (const auto& [_, entryPoint] : writeablePacks) {
|
||||
io::remove_device(entryPoint);
|
||||
}
|
||||
contentEntryPoints.clear();
|
||||
this->contentPacks = contentPacks;
|
||||
// Create content devices
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@ -33,6 +34,8 @@ public:
|
||||
io::path getControlsFile() const;
|
||||
io::path getSettingsFile() const;
|
||||
|
||||
std::string createWriteablePackDevice(const std::string& name);
|
||||
|
||||
void setContentPacks(std::vector<ContentPack>* contentPacks);
|
||||
|
||||
std::vector<io::path> scanForWorlds() const;
|
||||
@ -47,6 +50,7 @@ private:
|
||||
std::optional<std::filesystem::path> scriptFolder;
|
||||
std::vector<ContentPack>* contentPacks = nullptr;
|
||||
std::vector<std::string> contentEntryPoints;
|
||||
std::unordered_map<std::string, std::string> writeablePacks;
|
||||
};
|
||||
|
||||
struct PathsRoot {
|
||||
|
||||
@ -59,6 +59,8 @@ struct ItemDef {
|
||||
|
||||
std::string modelName = name + ".model";
|
||||
|
||||
std::string scriptFile;
|
||||
|
||||
struct {
|
||||
itemid_t id;
|
||||
blockid_t placingBlock;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "content/Content.hpp"
|
||||
#include "content/ContentLoader.hpp"
|
||||
#include "lighting/Lighting.hpp"
|
||||
#include "logic/BlocksController.hpp"
|
||||
#include "logic/LevelController.hpp"
|
||||
@ -12,6 +13,7 @@
|
||||
#include "world/Level.hpp"
|
||||
#include "maths/voxmaths.hpp"
|
||||
#include "data/StructLayout.hpp"
|
||||
#include "engine/Engine.hpp"
|
||||
#include "api_lua.hpp"
|
||||
|
||||
using namespace scripting;
|
||||
@ -617,6 +619,17 @@ static int l_set_field(lua::State* L) {
|
||||
return set_field(L, dst, *field, index, dataStruct, value);
|
||||
}
|
||||
|
||||
static int l_reload_script(lua::State* L) {
|
||||
auto name = lua::require_string(L, 1);
|
||||
if (content == nullptr) {
|
||||
throw std::runtime_error("content is not initialized");
|
||||
}
|
||||
auto& writeableContent = *engine->getWriteableContent();
|
||||
auto& def = writeableContent.blocks.require(name);
|
||||
ContentLoader::reloadScript(writeableContent, def);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const luaL_Reg blocklib[] = {
|
||||
{"index", lua::wrap<l_index>},
|
||||
{"name", lua::wrap<l_get_def>},
|
||||
@ -652,5 +665,6 @@ const luaL_Reg blocklib[] = {
|
||||
{"decompose_state", lua::wrap<l_decompose_state>},
|
||||
{"get_field", lua::wrap<l_get_field>},
|
||||
{"set_field", lua::wrap<l_set_field>},
|
||||
{"reload_script", lua::wrap<l_reload_script>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -273,6 +273,33 @@ static int l_blank(lua::State*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_capture_output(lua::State* L) {
|
||||
int argc = lua::gettop(L) - 1;
|
||||
if (!lua::isfunction(L, 1)) {
|
||||
throw std::runtime_error("function expected as argument 1");
|
||||
}
|
||||
for (int i = 0; i < argc; i++) {
|
||||
lua::pushvalue(L, i + 2);
|
||||
}
|
||||
lua::pushvalue(L, 1);
|
||||
|
||||
auto prev_output = output_stream;
|
||||
auto prev_error = error_stream;
|
||||
|
||||
std::stringstream captured_output;
|
||||
|
||||
output_stream = &captured_output;
|
||||
error_stream = &captured_output;
|
||||
|
||||
lua::call_nothrow(L, argc, 0);
|
||||
|
||||
output_stream = prev_output;
|
||||
error_stream = prev_error;
|
||||
|
||||
lua::pushstring(L, captured_output.str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
const luaL_Reg corelib[] = {
|
||||
{"blank", lua::wrap<l_blank>},
|
||||
{"get_version", lua::wrap<l_get_version>},
|
||||
@ -292,6 +319,7 @@ const luaL_Reg corelib[] = {
|
||||
{"get_setting_info", lua::wrap<l_get_setting_info>},
|
||||
{"open_folder", lua::wrap<l_open_folder>},
|
||||
{"quit", lua::wrap<l_quit>},
|
||||
{"capture_output", lua::wrap<l_capture_output>},
|
||||
{"__load_texture", lua::wrap<l_load_texture>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -223,6 +223,18 @@ static int l_raycast(lua::State* L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_reload_component(lua::State* L) {
|
||||
std::string name = lua::require_string(L, 1);
|
||||
size_t pos = name.find(':');
|
||||
if (pos == std::string::npos) {
|
||||
throw std::runtime_error("missing entry point");
|
||||
}
|
||||
auto filename = name.substr(0, pos + 1) + "scripts/components/" +
|
||||
name.substr(pos + 1) + ".lua";
|
||||
scripting::load_entity_component(name, filename, filename);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const luaL_Reg entitylib[] = {
|
||||
{"exists", lua::wrap<l_exists>},
|
||||
{"def_index", lua::wrap<l_def_index>},
|
||||
@ -238,5 +250,6 @@ const luaL_Reg entitylib[] = {
|
||||
{"get_all_in_box", lua::wrap<l_get_all_in_box>},
|
||||
{"get_all_in_radius", lua::wrap<l_get_all_in_radius>},
|
||||
{"raycast", lua::wrap<l_raycast>},
|
||||
{"reload_component", lua::wrap<l_reload_component>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -41,10 +41,23 @@ static std::set<std::string> writeable_entry_points {
|
||||
"world", "export", "config"
|
||||
};
|
||||
|
||||
static bool is_writeable(const std::string& entryPoint) {
|
||||
if (entryPoint.length() < 2) {
|
||||
return false;
|
||||
}
|
||||
if (entryPoint.substr(0, 2) == "W.") {
|
||||
return true;
|
||||
}
|
||||
if (writeable_entry_points.find(entryPoint) != writeable_entry_points.end()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static io::path get_writeable_path(lua::State* L) {
|
||||
io::path path = lua::require_string(L, 1);
|
||||
auto entryPoint = path.entryPoint();
|
||||
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
|
||||
if (!is_writeable(entryPoint)) {
|
||||
throw std::runtime_error("access denied");
|
||||
}
|
||||
return path;
|
||||
@ -60,7 +73,7 @@ static int l_write(lua::State* L) {
|
||||
static int l_remove(lua::State* L) {
|
||||
io::path path = lua::require_string(L, 1);
|
||||
auto entryPoint = path.entryPoint();
|
||||
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
|
||||
if (!is_writeable(entryPoint)) {
|
||||
throw std::runtime_error("access denied");
|
||||
}
|
||||
return lua::pushboolean(L, io::remove(path));
|
||||
@ -69,7 +82,7 @@ static int l_remove(lua::State* L) {
|
||||
static int l_remove_tree(lua::State* L) {
|
||||
io::path path = lua::require_string(L, 1);
|
||||
auto entryPoint = path.entryPoint();
|
||||
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
|
||||
if (!is_writeable(entryPoint)) {
|
||||
throw std::runtime_error("access denied");
|
||||
}
|
||||
return lua::pushinteger(L, io::remove_all(path));
|
||||
@ -222,10 +235,7 @@ static int l_read_combined_object(lua::State* L) {
|
||||
static int l_is_writeable(lua::State* L) {
|
||||
io::path path = lua::require_string(L, 1);
|
||||
auto entryPoint = path.entryPoint();
|
||||
if (writeable_entry_points.find(entryPoint) == writeable_entry_points.end()) {
|
||||
return lua::pushboolean(L, false);
|
||||
}
|
||||
return lua::pushboolean(L, true);
|
||||
return lua::pushboolean(L, is_writeable(entryPoint));
|
||||
}
|
||||
|
||||
const luaL_Reg filelib[] = {
|
||||
@ -249,4 +259,5 @@ const luaL_Reg filelib[] = {
|
||||
{"read_combined_list", lua::wrap<l_read_combined_list>},
|
||||
{"read_combined_object", lua::wrap<l_read_combined_object>},
|
||||
{"is_writeable", lua::wrap<l_is_writeable>},
|
||||
{NULL, NULL}};
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -96,7 +96,7 @@ static int l_node_destruct(lua::State* L) {
|
||||
engine->getGUI()->postRunnable([node]() {
|
||||
auto parent = node->getParent();
|
||||
if (auto container = dynamic_cast<Container*>(parent)) {
|
||||
container->remove(node);
|
||||
container->remove(node.get());
|
||||
}
|
||||
});
|
||||
return 0;
|
||||
@ -294,6 +294,13 @@ static int p_get_editable(UINode* node, lua::State* L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int p_get_edited(UINode* node, lua::State* L) {
|
||||
if (auto box = dynamic_cast<TextBox*>(node)) {
|
||||
return lua::pushboolean(L, box->isEdited());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int p_get_line_numbers(UINode* node, lua::State* L) {
|
||||
if (auto box = dynamic_cast<TextBox*>(node)) {
|
||||
return lua::pushboolean(L, box->isShowLineNumbers());
|
||||
@ -451,6 +458,7 @@ static int l_gui_getattr(lua::State* L) {
|
||||
{"caret", p_get_caret},
|
||||
{"text", p_get_text},
|
||||
{"editable", p_get_editable},
|
||||
{"edited", p_get_edited},
|
||||
{"lineNumbers", p_get_line_numbers},
|
||||
{"lineAt", p_get_line_at},
|
||||
{"linePos", p_get_line_pos},
|
||||
@ -545,6 +553,13 @@ static void p_set_editable(UINode* node, lua::State* L, int idx) {
|
||||
box->setEditable(lua::toboolean(L, idx));
|
||||
}
|
||||
}
|
||||
static void p_set_edited(UINode* node, lua::State* L, int idx) {
|
||||
if (auto box = dynamic_cast<TextBox*>(node)) {
|
||||
if (!lua::toboolean(L, idx)) {
|
||||
box->setUnedited();
|
||||
}
|
||||
}
|
||||
}
|
||||
static void p_set_line_numbers(UINode* node, lua::State* L, int idx) {
|
||||
if (auto box = dynamic_cast<TextBox*>(node)) {
|
||||
box->setShowLineNumbers(lua::toboolean(L, idx));
|
||||
@ -667,6 +682,7 @@ static int l_gui_setattr(lua::State* L) {
|
||||
{"hint", p_set_hint},
|
||||
{"text", p_set_text},
|
||||
{"editable", p_set_editable},
|
||||
{"edited", p_set_edited},
|
||||
{"lineNumbers", p_set_line_numbers},
|
||||
{"syntax", p_set_syntax},
|
||||
{"markup", p_set_markup},
|
||||
|
||||
@ -171,6 +171,23 @@ static int l_set_allow_pause(lua::State* L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_reload_script(lua::State* L) {
|
||||
auto packid = lua::require_string(L, 1);
|
||||
if (content == nullptr) {
|
||||
throw std::runtime_error("content is not initialized");
|
||||
}
|
||||
auto& writeableContent = *engine->getWriteableContent();
|
||||
auto pack = writeableContent.getPackRuntime(packid);
|
||||
const auto& info = pack->getInfo();
|
||||
scripting::load_hud_script(
|
||||
pack->getEnvironment(),
|
||||
packid,
|
||||
info.folder / "scripts/hud.lua",
|
||||
pack->getId() + ":scripts/hud.lua"
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const luaL_Reg hudlib[] = {
|
||||
{"open_inventory", wrap_hud<l_open_inventory>},
|
||||
{"close_inventory", wrap_hud<l_close_inventory>},
|
||||
@ -189,5 +206,6 @@ const luaL_Reg hudlib[] = {
|
||||
{"_set_content_access", wrap_hud<l_set_content_access>},
|
||||
{"_set_debug_cheats", wrap_hud<l_set_debug_cheats>},
|
||||
{"set_allow_pause", wrap_hud<l_set_allow_pause>},
|
||||
{"reload_script", wrap_hud<l_reload_script>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#include "content/Content.hpp"
|
||||
#include "content/ContentLoader.hpp"
|
||||
#include "items/ItemDef.hpp"
|
||||
#include "api_lua.hpp"
|
||||
#include "engine/Engine.hpp"
|
||||
|
||||
using namespace scripting;
|
||||
|
||||
@ -87,6 +89,17 @@ static int l_uses(lua::State* L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_reload_script(lua::State* L) {
|
||||
auto name = lua::require_string(L, 1);
|
||||
if (content == nullptr) {
|
||||
throw std::runtime_error("content is not initialized");
|
||||
}
|
||||
auto& writeableContent = *engine->getWriteableContent();
|
||||
auto& def = writeableContent.items.require(name);
|
||||
ContentLoader::reloadScript(writeableContent, def);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const luaL_Reg itemlib[] = {
|
||||
{"index", lua::wrap<l_index>},
|
||||
{"name", lua::wrap<l_name>},
|
||||
@ -98,5 +111,6 @@ const luaL_Reg itemlib[] = {
|
||||
{"model_name", lua::wrap<l_model_name>},
|
||||
{"emission", lua::wrap<l_emission>},
|
||||
{"uses", lua::wrap<l_uses>},
|
||||
{"reload_script", lua::wrap<l_reload_script>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -7,6 +7,9 @@
|
||||
#include "assets/AssetsLoader.hpp"
|
||||
#include "content/Content.hpp"
|
||||
#include "engine/Engine.hpp"
|
||||
#include "graphics/ui/gui_util.hpp"
|
||||
#include "graphics/ui/elements/Menu.hpp"
|
||||
#include "frontend/locale.hpp"
|
||||
#include "world/files/WorldFiles.hpp"
|
||||
#include "io/engine_paths.hpp"
|
||||
#include "world/Level.hpp"
|
||||
@ -247,6 +250,20 @@ static int l_pack_assemble(lua::State* L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_pack_request_writeable(lua::State* L) {
|
||||
auto packid = lua::require_string(L, 1);
|
||||
lua::pushvalue(L, 2);
|
||||
auto handler = lua::create_lambda_nothrow(L);
|
||||
|
||||
auto str = langs::get(L"Grant %{0} pack modification permission?");
|
||||
util::replaceAll(str, L"%{0}", util::str2wstr_utf8(packid));
|
||||
guiutil::confirm(*engine, str, [packid, handler]() {
|
||||
handler({engine->getPaths().createWriteablePackDevice(packid)});
|
||||
engine->getGUI()->getMenu()->reset();
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
const luaL_Reg packlib[] = {
|
||||
{"get_folder", lua::wrap<l_pack_get_folder>},
|
||||
{"get_installed", lua::wrap<l_pack_get_installed>},
|
||||
@ -254,4 +271,6 @@ const luaL_Reg packlib[] = {
|
||||
{"get_info", lua::wrap<l_pack_get_info>},
|
||||
{"get_base_packs", lua::wrap<l_pack_get_base_packs>},
|
||||
{"assemble", lua::wrap<l_pack_assemble>},
|
||||
{NULL, NULL}};
|
||||
{"request_writeable", lua::wrap<l_pack_request_writeable>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "assets/AssetsLoader.hpp"
|
||||
#include "coders/json.hpp"
|
||||
#include "content/Content.hpp"
|
||||
#include "content/ContentLoader.hpp"
|
||||
#include "engine/Engine.hpp"
|
||||
#include "world/files/WorldFiles.hpp"
|
||||
#include "io/engine_paths.hpp"
|
||||
@ -213,6 +214,17 @@ static int l_count_chunks(lua::State* L) {
|
||||
return lua::pushinteger(L, level->chunks->size());
|
||||
}
|
||||
|
||||
static int l_reload_script(lua::State* L) {
|
||||
auto packid = lua::require_string(L, 1);
|
||||
if (content == nullptr) {
|
||||
throw std::runtime_error("content is not initialized");
|
||||
}
|
||||
auto& writeableContent = *engine->getWriteableContent();
|
||||
auto pack = writeableContent.getPackRuntime(packid);
|
||||
ContentLoader::loadWorldScript(*pack);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const luaL_Reg worldlib[] = {
|
||||
{"is_open", lua::wrap<l_is_open>},
|
||||
{"get_list", lua::wrap<l_get_list>},
|
||||
@ -230,5 +242,6 @@ const luaL_Reg worldlib[] = {
|
||||
{"set_chunk_data", lua::wrap<l_set_chunk_data>},
|
||||
{"save_chunk_data", lua::wrap<l_save_chunk_data>},
|
||||
{"count_chunks", lua::wrap<l_count_chunks>},
|
||||
{"reload_script", lua::wrap<l_reload_script>},
|
||||
{NULL, NULL}
|
||||
};
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
#include "libs/api_lua.hpp"
|
||||
|
||||
using namespace scripting;
|
||||
|
||||
/// @brief Modified version of luaB_print from lbaselib.c
|
||||
int l_print(lua::State* L) {
|
||||
int n = lua::gettop(L); /* number of arguments */
|
||||
@ -16,10 +18,10 @@ int l_print(lua::State* L) {
|
||||
L,
|
||||
LUA_QL("tostring") " must return a string to " LUA_QL("print")
|
||||
);
|
||||
if (i > 1) std::cout << "\t";
|
||||
std::cout << s;
|
||||
if (i > 1) *output_stream << "\t";
|
||||
*output_stream << s;
|
||||
lua::pop(L); /* pop result */
|
||||
}
|
||||
std::cout << std::endl;
|
||||
*output_stream << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -34,6 +34,8 @@ static debug::Logger logger("scripting");
|
||||
|
||||
static inline const std::string STDCOMP = "stdcomp";
|
||||
|
||||
std::ostream* scripting::output_stream = &std::cout;
|
||||
std::ostream* scripting::error_stream = &std::cerr;
|
||||
Engine* scripting::engine = nullptr;
|
||||
Level* scripting::level = nullptr;
|
||||
const Content* scripting::content = nullptr;
|
||||
@ -804,21 +806,21 @@ bool scripting::register_event(
|
||||
if (lua::pushenv(L, env) == 0) {
|
||||
lua::pushglobals(L);
|
||||
}
|
||||
if (lua::getfield(L, name)) {
|
||||
lua::pop(L);
|
||||
lua::getglobal(L, "events");
|
||||
lua::getfield(L, "reset");
|
||||
lua::pushstring(L, id);
|
||||
lua::getfield(L, name, -4);
|
||||
lua::call_nothrow(L, 2);
|
||||
lua::pop(L);
|
||||
|
||||
// remove previous name
|
||||
bool success = true;
|
||||
lua::getglobal(L, "events");
|
||||
lua::getfield(L, "reset");
|
||||
lua::pushstring(L, id);
|
||||
if (!lua::getfield(L, name, -4)) {
|
||||
success = false;
|
||||
lua::pushnil(L);
|
||||
lua::setfield(L, name);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
lua::call_nothrow(L, 2);
|
||||
lua::pop(L);
|
||||
|
||||
// remove previous name
|
||||
lua::pushnil(L);
|
||||
lua::setfield(L, name);
|
||||
return success;
|
||||
}
|
||||
|
||||
int scripting::get_values_on_stack() {
|
||||
@ -834,6 +836,8 @@ void scripting::load_content_script(
|
||||
) {
|
||||
int env = *senv;
|
||||
lua::pop(lua::get_main_state(), load_script(env, "block", file, fileName));
|
||||
|
||||
funcsset = {};
|
||||
funcsset.init = register_event(env, "init", prefix + ".init");
|
||||
funcsset.update = register_event(env, "on_update", prefix + ".update");
|
||||
funcsset.randupdate =
|
||||
@ -859,6 +863,8 @@ void scripting::load_content_script(
|
||||
) {
|
||||
int env = *senv;
|
||||
lua::pop(lua::get_main_state(), load_script(env, "item", file, fileName));
|
||||
|
||||
funcsset = {};
|
||||
funcsset.init = register_event(env, "init", prefix + ".init");
|
||||
funcsset.on_use = register_event(env, "on_use", prefix + ".use");
|
||||
funcsset.on_use_on_block =
|
||||
@ -886,6 +892,8 @@ void scripting::load_world_script(
|
||||
) {
|
||||
int env = *senv;
|
||||
lua::pop(lua::get_main_state(), load_script(env, "world", file, fileName));
|
||||
|
||||
funcsset = {};
|
||||
register_event(env, "init", prefix + ".init");
|
||||
register_event(env, "on_world_open", prefix + ":.worldopen");
|
||||
register_event(env, "on_world_tick", prefix + ":.worldtick");
|
||||
|
||||
@ -42,6 +42,8 @@ namespace scripting {
|
||||
extern Level* level;
|
||||
extern BlocksController* blocks;
|
||||
extern LevelController* controller;
|
||||
extern std::ostream* output_stream;
|
||||
extern std::ostream* error_stream;
|
||||
|
||||
void initialize(Engine* engine);
|
||||
|
||||
|
||||
@ -36,6 +36,28 @@ static lua::State* process_callback(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
key_handler scripting::create_key_handler(
|
||||
const scriptenv& env, const std::string& src, const std::string& file
|
||||
) {
|
||||
return [=](int code) {
|
||||
if (auto L = process_callback(env, src, file)) {
|
||||
int top = lua::gettop(L);
|
||||
if (lua::isfunction(L, -1)) {
|
||||
lua::pushinteger(L, code);
|
||||
lua::call_nothrow(L, 1);
|
||||
}
|
||||
int returned = lua::gettop(L) - top + 1;
|
||||
if (returned) {
|
||||
bool x = lua::toboolean(L, -1);
|
||||
lua::pop(L, returned);
|
||||
return x;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
wstringconsumer scripting::create_wstring_consumer(
|
||||
const scriptenv& env, const std::string& src, const std::string& file
|
||||
) {
|
||||
|
||||
@ -17,6 +17,12 @@ namespace scripting {
|
||||
const std::string& file = "[string]"
|
||||
);
|
||||
|
||||
key_handler create_key_handler(
|
||||
const scriptenv& env,
|
||||
const std::string& src,
|
||||
const std::string& file = "[string]"
|
||||
);
|
||||
|
||||
wstringconsumer create_wstring_consumer(
|
||||
const scriptenv& env,
|
||||
const std::string& src,
|
||||
|
||||
@ -306,6 +306,12 @@ const char B64ABC[] =
|
||||
"0123456789"
|
||||
"+/";
|
||||
|
||||
const char URLSAFE_B64ABC[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789"
|
||||
"-_";
|
||||
|
||||
inline ubyte base64_decode_char(char c) {
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||
@ -315,16 +321,27 @@ inline ubyte base64_decode_char(char c) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline void base64_encode_(const ubyte* segment, char* output) {
|
||||
output[0] = B64ABC[(segment[0] & 0b11111100) >> 2];
|
||||
output[1] =
|
||||
B64ABC[((segment[0] & 0b11) << 4) | ((segment[1] & 0b11110000) >> 4)];
|
||||
output[2] =
|
||||
B64ABC[((segment[1] & 0b1111) << 2) | ((segment[2] & 0b11000000) >> 6)];
|
||||
output[3] = B64ABC[segment[2] & 0b111111];
|
||||
inline ubyte base64_urlsafe_decode_char(char c) {
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||
if (c == '-') return 62;
|
||||
if (c == '_') return 63;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string util::base64_encode(const ubyte* data, size_t size) {
|
||||
template <const char* ABC>
|
||||
static void base64_encode_(const ubyte* segment, char* output) {
|
||||
output[0] = ABC[(segment[0] & 0b11111100) >> 2];
|
||||
output[1] =
|
||||
ABC[((segment[0] & 0b11) << 4) | ((segment[1] & 0b11110000) >> 4)];
|
||||
output[2] =
|
||||
ABC[((segment[1] & 0b1111) << 2) | ((segment[2] & 0b11000000) >> 6)];
|
||||
output[3] = ABC[segment[2] & 0b111111];
|
||||
}
|
||||
|
||||
template <const char* ABC>
|
||||
static std::string base64_encode_impl(const ubyte* data, size_t size) {
|
||||
std::stringstream ss;
|
||||
|
||||
size_t fullsegments = (size / 3) * 3;
|
||||
@ -332,7 +349,7 @@ std::string util::base64_encode(const ubyte* data, size_t size) {
|
||||
size_t i = 0;
|
||||
for (; i < fullsegments; i += 3) {
|
||||
char output[] = "====";
|
||||
base64_encode_(data + i, output);
|
||||
base64_encode_<ABC>(data + i, output);
|
||||
ss << output;
|
||||
}
|
||||
|
||||
@ -343,18 +360,27 @@ std::string util::base64_encode(const ubyte* data, size_t size) {
|
||||
size_t trailing = size - fullsegments;
|
||||
if (trailing) {
|
||||
char output[] = "====";
|
||||
output[0] = B64ABC[(ending[0] & 0b11111100) >> 2];
|
||||
output[0] = ABC[(ending[0] & 0b11111100) >> 2];
|
||||
output[1] =
|
||||
B64ABC[((ending[0] & 0b11) << 4) | ((ending[1] & 0b11110000) >> 4)];
|
||||
ABC[((ending[0] & 0b11) << 4) | ((ending[1] & 0b11110000) >> 4)];
|
||||
if (trailing > 1)
|
||||
output[2] = B64ABC
|
||||
[((ending[1] & 0b1111) << 2) | ((ending[2] & 0b11000000) >> 6)];
|
||||
if (trailing > 2) output[3] = B64ABC[ending[2] & 0b111111];
|
||||
output[2] =
|
||||
ABC[((ending[1] & 0b1111) << 2) |
|
||||
((ending[2] & 0b11000000) >> 6)];
|
||||
if (trailing > 2) output[3] = ABC[ending[2] & 0b111111];
|
||||
ss << output;
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string util::base64_encode(const ubyte* data, size_t size) {
|
||||
return base64_encode_impl<B64ABC>(data, size);
|
||||
}
|
||||
|
||||
std::string util::base64_urlsafe_encode(const ubyte* data, size_t size) {
|
||||
return base64_encode_impl<URLSAFE_B64ABC>(data, size);
|
||||
}
|
||||
|
||||
std::string util::tohex(uint64_t value) {
|
||||
std::stringstream ss;
|
||||
ss << std::hex << value;
|
||||
@ -366,7 +392,8 @@ std::string util::mangleid(uint64_t value) {
|
||||
return tohex(value);
|
||||
}
|
||||
|
||||
util::Buffer<ubyte> util::base64_decode(const char* str, size_t size) {
|
||||
template <ubyte(base64_decode_char)(char)>
|
||||
static util::Buffer<ubyte> base64_decode_impl(const char* str, size_t size) {
|
||||
util::Buffer<ubyte> bytes((size / 4) * 3);
|
||||
ubyte* dst = bytes.data();
|
||||
for (size_t i = 0; i < (size / 4) * 4;) {
|
||||
@ -387,18 +414,35 @@ util::Buffer<ubyte> util::base64_decode(const char* str, size_t size) {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
util::Buffer<ubyte> util::base64_urlsafe_decode(const char* str, size_t size) {
|
||||
return base64_decode_impl<base64_urlsafe_decode_char>(str, size);
|
||||
}
|
||||
|
||||
util::Buffer<ubyte> util::base64_decode(const char* str, size_t size) {
|
||||
return base64_decode_impl<base64_decode_char>(str, size);
|
||||
}
|
||||
|
||||
util::Buffer<ubyte> util::base64_urlsafe_decode(std::string_view str) {
|
||||
return base64_urlsafe_decode(str.data(), str.size());
|
||||
}
|
||||
|
||||
util::Buffer<ubyte> util::base64_decode(std::string_view str) {
|
||||
return base64_decode(str.data(), str.size());
|
||||
}
|
||||
|
||||
int util::replaceAll(
|
||||
std::string& str, const std::string& from, const std::string& to
|
||||
template <typename CharT>
|
||||
static int replace_all(
|
||||
std::basic_string<CharT>& str,
|
||||
const std::basic_string<CharT>& from,
|
||||
const std::basic_string<CharT>& to
|
||||
) {
|
||||
int count = 0;
|
||||
size_t offset = 0;
|
||||
while (true) {
|
||||
size_t start_pos = str.find(from, offset);
|
||||
if (start_pos == std::string::npos) break;
|
||||
if (start_pos == std::basic_string<CharT>::npos) {
|
||||
break;
|
||||
}
|
||||
str.replace(start_pos, from.length(), to);
|
||||
offset = start_pos + to.length();
|
||||
count++;
|
||||
@ -406,6 +450,18 @@ int util::replaceAll(
|
||||
return count;
|
||||
}
|
||||
|
||||
int util::replaceAll(
|
||||
std::string& str, const std::string& from, const std::string& to
|
||||
) {
|
||||
return replace_all(str, from, to);
|
||||
}
|
||||
|
||||
int util::replaceAll(
|
||||
std::wstring& str, const std::wstring& from, const std::wstring& to
|
||||
) {
|
||||
return replace_all(str, from, to);
|
||||
}
|
||||
|
||||
// replace it with std::from_chars in the far far future
|
||||
double util::parse_double(const std::string& str) {
|
||||
std::istringstream ss(str);
|
||||
|
||||
@ -74,8 +74,12 @@ namespace util {
|
||||
std::wstring to_wstring(double x, int precision);
|
||||
|
||||
std::string base64_encode(const ubyte* data, size_t size);
|
||||
std::string base64_urlsafe_encode(const ubyte* data, size_t size);
|
||||
|
||||
util::Buffer<ubyte> base64_decode(const char* str, size_t size);
|
||||
util::Buffer<ubyte> base64_urlsafe_decode(const char* str, size_t size);
|
||||
util::Buffer<ubyte> base64_decode(std::string_view str);
|
||||
util::Buffer<ubyte> base64_urlsafe_decode(std::string_view str);
|
||||
|
||||
std::string tohex(uint64_t value);
|
||||
|
||||
@ -85,6 +89,10 @@ namespace util {
|
||||
std::string& str, const std::string& from, const std::string& to
|
||||
);
|
||||
|
||||
int replaceAll(
|
||||
std::wstring& str, const std::wstring& from, const std::wstring& to
|
||||
);
|
||||
|
||||
double parse_double(const std::string& str);
|
||||
double parse_double(const std::string& str, size_t offset, size_t len);
|
||||
|
||||
|
||||
@ -204,6 +204,8 @@ public:
|
||||
/// @brief Block script name in blocks/ without extension
|
||||
std::string scriptName = name.substr(name.find(':') + 1);
|
||||
|
||||
std::string scriptFile;
|
||||
|
||||
/// @brief Block will be used instead of this if generated on surface
|
||||
std::string surfaceReplacement = name;
|
||||
|
||||
|
||||
@ -33,14 +33,15 @@ glm::mat4 Camera::getProjection() const {
|
||||
constexpr float epsilon = 1e-6f; // 0.000001
|
||||
float aspect_ratio = this->aspect;
|
||||
if (std::fabs(aspect_ratio) < epsilon) {
|
||||
aspect_ratio = (float)Window::width / (float)Window::height;
|
||||
aspect_ratio = Window::width / static_cast<float>(Window::height);
|
||||
}
|
||||
if (perspective)
|
||||
if (perspective) {
|
||||
return glm::perspective(fov * zoom, aspect_ratio, near, far);
|
||||
else if (flipped)
|
||||
return glm::ortho(0.0f, fov * aspect_ratio, fov, 0.0f);
|
||||
else
|
||||
return glm::ortho(0.0f, fov * aspect_ratio, 0.0f, fov);
|
||||
} else if (flipped) {
|
||||
return glm::ortho(-0.5f, fov * aspect_ratio-0.5f, fov, 0.0f);
|
||||
} else {
|
||||
return glm::ortho(-0.5f, fov * aspect_ratio-0.5f, 0.0f, fov);
|
||||
}
|
||||
}
|
||||
|
||||
glm::mat4 Camera::getView(bool pos) const {
|
||||
|
||||
@ -31,3 +31,19 @@ TEST(stringutil, base64) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(stringutil, base64_urlsafe) {
|
||||
srand(2019);
|
||||
for (size_t size = 0; size < 30; size++) {
|
||||
auto bytes = std::make_unique<ubyte[]>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
bytes[i] = rand();
|
||||
}
|
||||
auto base64 = util::base64_urlsafe_encode(bytes.get(), size);
|
||||
auto decoded = util::base64_urlsafe_decode(base64);
|
||||
ASSERT_EQ(size, decoded.size());
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
ASSERT_EQ(bytes[i], decoded[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||