mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
gui framework
This commit is contained in:
@@ -19,7 +19,7 @@ find_package(CLI11 CONFIG REQUIRED)
|
||||
find_package(rapidjson CONFIG REQUIRED)
|
||||
|
||||
if(BUILD_SDL3_APP)
|
||||
add_executable(sdl3_app
|
||||
add_executable(sdl3_app
|
||||
src/main.cpp
|
||||
src/app/sdl3_app_core.cpp
|
||||
src/app/sdl3_app_device.cpp
|
||||
@@ -27,6 +27,7 @@ if(BUILD_SDL3_APP)
|
||||
src/app/sdl3_app_pipeline.cpp
|
||||
src/app/sdl3_app_build.cpp
|
||||
src/app/sdl3_app_buffers.cpp
|
||||
src/gui/gui_renderer.cpp
|
||||
src/app/sdl3_app_render.cpp
|
||||
src/app/vulkan_api.cpp
|
||||
src/script/cube_script.cpp
|
||||
|
||||
@@ -33,6 +33,9 @@ If you need the Conan runtime environment (e.g., because dependencies set env va
|
||||
|
||||
`main.cpp` now uses a JSON-driven entrypoint. Use `sdl3_app --json-file-in <path>` to load a configuration that points at the Lua script and captures window dimensions, or run `sdl3_app --create-seed-json config/seed_runtime.json` to write a starter JSON file (based on the executable’s `scripts/cube_logic.lua` location). You can also use `sdl3_app --set-default-json` (optionally followed by an existing JSON path) to copy the runtime JSON to the platform default directory (APPDATA on Windows, `XDG_CONFIG_HOME`/`$HOME/.config` elsewhere); when that default file exists, the app picks it up automatically when launched without extra CLI options. If no JSON input is provided and no default exists, the app falls back to discovering `scripts/cube_logic.lua` next to the binary.
|
||||
|
||||
### GUI Demo
|
||||
`scripts/gui_demo.lua` shows off the Lua GUI framework (buttons, text boxes, list views, and SVG icons) rendered on top of the Vulkan scene. Launch it with `./build/sdl3_app --json-file-in config/gui_runtime.json` (or use the new config as input to `sdl3_app --set-default-json`) to run the interactive overlay in the window.
|
||||
|
||||
## Dependency automation
|
||||
|
||||
This project ships a `renovate.json` configuration so Renovate can open PRs that bump the Conan packages listed in `conanfile.py`. Either install Renovate locally (`npm install -g renovate` or `npx renovate`) and run it from the repo root, or enable the Renovate bot on your GitHub/GitLab install to pick up the configuration automatically.
|
||||
|
||||
12
config/gui_runtime.json
Normal file
12
config/gui_runtime.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
window_width: 1024,
|
||||
window_height: 768,
|
||||
lua_script: scripts/gui_demo.lua,
|
||||
scripts_directory: scripts,
|
||||
project_root: .,
|
||||
shaders_directory: shaders,
|
||||
device_extensions: [
|
||||
VK_KHR_swapchain
|
||||
],
|
||||
config_file: config/gui_runtime.json
|
||||
}
|
||||
7
scripts/assets/logo.svg
Normal file
7
scripts/assets/logo.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width=\ 128\ height=\128\ viewBox=\0 0 128 128\ xmlns=\http://www.w3.org/2000/svg\>
|
||||
<circle cx=\64\ cy=\64\ r=\60\ fill=\#1f7ae0\ />
|
||||
<circle cx=\64\ cy=\64\ r=\42\ fill=\#ffffff\ />
|
||||
<circle cx=\64\ cy=\64\ r=\32\ fill=\#0f3d91\ />
|
||||
<circle cx=\64\ cy=\30\ r=\12\ fill=\#f4b400\ />
|
||||
<circle cx=\96\ cy=\92\ r=\10\ fill=\#34a853\ />
|
||||
</svg>
|
||||
535
scripts/gui.lua
Normal file
535
scripts/gui.lua
Normal file
@@ -0,0 +1,535 @@
|
||||
-- Lightweight Lua-based 2D GUI framework that emits draw commands
|
||||
-- and handles interaction for buttons, textboxes, and list views.
|
||||
local Gui = {}
|
||||
|
||||
-- {r,g,b,a} colors
|
||||
local DEFAULT_STYLE = {
|
||||
spacing = 8,
|
||||
radius = 6,
|
||||
fontSize = 16,
|
||||
button = {
|
||||
background = {0.2, 0.24, 0.28, 1.0},
|
||||
hovered = {0.26, 0.3, 0.36, 1.0},
|
||||
active = {0.16, 0.22, 0.28, 1.0},
|
||||
border = {0.45, 0.52, 0.6, 1.0},
|
||||
text = {1.0, 1.0, 1.0, 1.0},
|
||||
},
|
||||
textbox = {
|
||||
background = {0.05, 0.06, 0.08, 1.0},
|
||||
border = {0.38, 0.4, 0.45, 1.0},
|
||||
focusedBorder = {0.35, 0.55, 0.9, 1.0},
|
||||
text = {0.97, 0.97, 0.97, 1.0},
|
||||
placeholder = {0.5, 0.5, 0.5, 1.0},
|
||||
cursor = {0.98, 0.98, 0.98, 1.0},
|
||||
},
|
||||
listview = {
|
||||
background = {0.04, 0.05, 0.07, 1.0},
|
||||
border = {0.25, 0.28, 0.32, 1.0},
|
||||
alternate = {0.06, 0.07, 0.09, 1.0},
|
||||
selection = {0.12, 0.35, 0.65, 1.0},
|
||||
text = {0.93, 0.94, 0.96, 1.0},
|
||||
},
|
||||
}
|
||||
|
||||
local function clamp(value, minValue, maxValue)
|
||||
if minValue and value < minValue then
|
||||
return minValue
|
||||
end
|
||||
if maxValue and value > maxValue then
|
||||
return maxValue
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local function rectContains(rect, x, y)
|
||||
return x >= rect.x and x <= rect.x + rect.width and y >= rect.y and y <= rect.y + rect.height
|
||||
end
|
||||
|
||||
local InputState = {}
|
||||
InputState.__index = InputState
|
||||
|
||||
function InputState:new()
|
||||
local instance = {
|
||||
mouseX = 0,
|
||||
mouseY = 0,
|
||||
mouseDown = false,
|
||||
mouseDownPrevious = false,
|
||||
wheel = 0,
|
||||
textInput = "",
|
||||
keys = {},
|
||||
}
|
||||
return setmetatable(instance, self)
|
||||
end
|
||||
|
||||
function InputState:setMouse(x, y, isDown)
|
||||
self.mouseDownPrevious = self.mouseDown
|
||||
self.mouseX = x
|
||||
self.mouseY = y
|
||||
self.mouseDown = isDown
|
||||
end
|
||||
|
||||
function InputState:setWheel(deltaY)
|
||||
self.wheel = deltaY
|
||||
end
|
||||
|
||||
function InputState:mouseJustPressed()
|
||||
return self.mouseDown and not self.mouseDownPrevious
|
||||
end
|
||||
|
||||
function InputState:mouseJustReleased()
|
||||
return not self.mouseDown and self.mouseDownPrevious
|
||||
end
|
||||
|
||||
function InputState:setKey(keyName, isDown)
|
||||
self.keys[keyName] = isDown
|
||||
end
|
||||
|
||||
function InputState:isKeyDown(keyName)
|
||||
return self.keys[keyName]
|
||||
end
|
||||
|
||||
function InputState:addTextInput(text)
|
||||
self.textInput = (self.textInput or "") .. (text or "")
|
||||
end
|
||||
|
||||
function InputState:resetTransient()
|
||||
self.textInput = ""
|
||||
self.wheel = 0
|
||||
end
|
||||
|
||||
local Context = {}
|
||||
Context.__index = Context
|
||||
|
||||
function Context:new(options)
|
||||
options = options or {}
|
||||
local style = options.style or DEFAULT_STYLE
|
||||
local instance = {
|
||||
commands = {},
|
||||
input = nil,
|
||||
hotWidget = nil,
|
||||
activeWidget = nil,
|
||||
focusWidget = nil,
|
||||
nextFocus = nil,
|
||||
style = style,
|
||||
mousePressed = false,
|
||||
mouseReleased = false,
|
||||
}
|
||||
return setmetatable(instance, self)
|
||||
end
|
||||
|
||||
function Context:beginFrame(input)
|
||||
if not input then
|
||||
error("Context requires an InputState for each frame")
|
||||
end
|
||||
self.input = input
|
||||
self.commands = {}
|
||||
self.hotWidget = nil
|
||||
self.nextFocus = nil
|
||||
self.mousePressed = input:mouseJustPressed()
|
||||
self.mouseReleased = input:mouseJustReleased()
|
||||
end
|
||||
|
||||
function Context:endFrame()
|
||||
if self.mouseReleased then
|
||||
self.activeWidget = nil
|
||||
end
|
||||
if self.nextFocus ~= nil then
|
||||
self.focusWidget = self.nextFocus
|
||||
elseif self.mousePressed and not self.hotWidget then
|
||||
self.focusWidget = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Context:requestFocus(widgetId)
|
||||
self.nextFocus = widgetId
|
||||
end
|
||||
|
||||
function Context:addCommand(command)
|
||||
table.insert(self.commands, command)
|
||||
end
|
||||
|
||||
function Context:getCommands()
|
||||
return self.commands
|
||||
end
|
||||
|
||||
function Context:isMouseOver(rect)
|
||||
return rectContains(rect, self.input.mouseX, self.input.mouseY)
|
||||
end
|
||||
|
||||
function Context:markHot(widgetId, hovered)
|
||||
if hovered then
|
||||
self.hotWidget = widgetId
|
||||
end
|
||||
end
|
||||
|
||||
function Context:pushRect(rect, params)
|
||||
params = params or {}
|
||||
local command = {
|
||||
type = "rect",
|
||||
x = rect.x,
|
||||
y = rect.y,
|
||||
width = rect.width,
|
||||
height = rect.height,
|
||||
color = params.color,
|
||||
borderColor = params.borderColor,
|
||||
borderWidth = params.borderWidth or (params.borderColor and 1 or 0),
|
||||
radius = params.radius or self.style.radius,
|
||||
}
|
||||
self:addCommand(command)
|
||||
end
|
||||
|
||||
function Context:pushText(rect, params)
|
||||
params = params or {}
|
||||
local padding = params.padding or self.style.spacing / 2
|
||||
local alignX = params.alignX or "left"
|
||||
local alignY = params.alignY or "center"
|
||||
local x = rect.x + padding
|
||||
local y = rect.y
|
||||
if alignX == "center" then
|
||||
x = rect.x + rect.width / 2
|
||||
elseif alignX == "right" then
|
||||
x = rect.x + rect.width - padding
|
||||
end
|
||||
if alignY == "center" then
|
||||
y = rect.y + rect.height / 2
|
||||
elseif alignY == "bottom" then
|
||||
y = rect.y + rect.height - padding
|
||||
else
|
||||
y = rect.y + padding
|
||||
end
|
||||
local command = {
|
||||
type = "text",
|
||||
text = params.text or "",
|
||||
x = x,
|
||||
y = y,
|
||||
color = params.color,
|
||||
fontSize = params.fontSize or self.style.fontSize,
|
||||
alignX = alignX,
|
||||
alignY = alignY,
|
||||
bounds = {x = rect.x, y = rect.y, width = rect.width, height = rect.height},
|
||||
}
|
||||
if params.clip then
|
||||
command.clipRect = {
|
||||
x = rect.x,
|
||||
y = rect.y,
|
||||
width = rect.width,
|
||||
height = rect.height,
|
||||
}
|
||||
end
|
||||
self:addCommand(command)
|
||||
end
|
||||
|
||||
function Context:pushSvg(svgPath, rect, opts)
|
||||
opts = opts or {}
|
||||
local command = {
|
||||
type = "svg",
|
||||
path = svgPath,
|
||||
x = rect.x,
|
||||
y = rect.y,
|
||||
width = rect.width,
|
||||
height = rect.height,
|
||||
color = opts.color,
|
||||
tint = opts.tint,
|
||||
}
|
||||
self:addCommand(command)
|
||||
end
|
||||
|
||||
function Context:pushClip(rect)
|
||||
self:addCommand({type = "clip_push", rect = rect})
|
||||
end
|
||||
|
||||
function Context:popClip()
|
||||
self:addCommand({type = "clip_pop"})
|
||||
end
|
||||
|
||||
function Context:processInteraction(widgetId, rect)
|
||||
local hovered = self:isMouseOver(rect)
|
||||
self:markHot(widgetId, hovered)
|
||||
if hovered and self.mousePressed then
|
||||
self.activeWidget = widgetId
|
||||
end
|
||||
local clicked = hovered and self.mouseReleased and self.activeWidget == widgetId
|
||||
return hovered, clicked
|
||||
end
|
||||
|
||||
local function ensureRect(rect)
|
||||
return {
|
||||
x = rect.x or rect[1] or 0,
|
||||
y = rect.y or rect[2] or 0,
|
||||
width = rect.width or rect[3] or 0,
|
||||
height = rect.height or rect[4] or 0,
|
||||
}
|
||||
end
|
||||
|
||||
function Gui.newContext(options)
|
||||
return Context:new(options)
|
||||
end
|
||||
|
||||
function Gui.newInputState()
|
||||
return InputState:new()
|
||||
end
|
||||
|
||||
function Gui.button(context, widgetId, rectDef, label, opts)
|
||||
opts = opts or {}
|
||||
local rect = ensureRect(rectDef)
|
||||
local style = context.style.button
|
||||
local hovered, clicked = context:processInteraction(widgetId, rect)
|
||||
local active = context.activeWidget == widgetId
|
||||
local fillColor = style.background
|
||||
if active then
|
||||
fillColor = style.active
|
||||
elseif hovered then
|
||||
fillColor = style.hovered
|
||||
end
|
||||
context:pushRect(rect, {
|
||||
color = fillColor,
|
||||
borderColor = style.border,
|
||||
radius = opts.radius,
|
||||
})
|
||||
context:pushText(rect, {
|
||||
text = label or "",
|
||||
color = style.text,
|
||||
alignX = "center",
|
||||
alignY = "center",
|
||||
fontSize = opts.fontSize,
|
||||
})
|
||||
if clicked and opts.onClick then
|
||||
opts.onClick()
|
||||
end
|
||||
return clicked
|
||||
end
|
||||
|
||||
function Gui.text(context, rectDef, text, opts)
|
||||
opts = opts or {}
|
||||
local rect = ensureRect(rectDef)
|
||||
context:pushText(rect, {
|
||||
text = text or "",
|
||||
color = opts.color,
|
||||
alignX = opts.alignX,
|
||||
alignY = opts.alignY,
|
||||
padding = opts.padding,
|
||||
fontSize = opts.fontSize,
|
||||
clip = opts.clip,
|
||||
})
|
||||
end
|
||||
|
||||
function Gui.svg(context, rectDef, svgPath, opts)
|
||||
opts = opts or {}
|
||||
local rect = ensureRect(rectDef)
|
||||
context:pushSvg(svgPath, rect, opts)
|
||||
end
|
||||
|
||||
function Gui.textbox(context, widgetId, rectDef, state, opts)
|
||||
opts = opts or {}
|
||||
state = state or {}
|
||||
state.text = state.text or ""
|
||||
state.cursor = clamp(state.cursor or #state.text, 0, #state.text)
|
||||
state.offset = clamp(state.offset or 0, 0, math.max(0, #state.text))
|
||||
local rect = ensureRect(rectDef)
|
||||
local style = context.style.textbox
|
||||
local hovered, clicked = context:processInteraction(widgetId, rect)
|
||||
local focused = context.focusWidget == widgetId
|
||||
if clicked then
|
||||
context:requestFocus(widgetId)
|
||||
end
|
||||
local background = style.background
|
||||
local borderColor = focused and style.focusedBorder or style.border
|
||||
context:pushRect(rect, {
|
||||
color = background,
|
||||
borderColor = borderColor,
|
||||
})
|
||||
local pad = opts.padding or context.style.spacing / 2
|
||||
local charWidth = opts.charWidth or (context.style.fontSize * 0.55)
|
||||
local innerWidth = math.max(0, rect.width - pad * 2)
|
||||
local maxChars = math.max(1, math.floor(innerWidth / charWidth))
|
||||
if focused then
|
||||
local function deleteRange(startIdx, count)
|
||||
if startIdx < 1 or startIdx > #state.text then
|
||||
return
|
||||
end
|
||||
local before = state.text:sub(1, startIdx - 1)
|
||||
local after = state.text:sub(startIdx + count)
|
||||
state.text = before .. after
|
||||
end
|
||||
local textInput = context.input.textInput or ""
|
||||
if textInput ~= "" then
|
||||
local before = state.text:sub(1, state.cursor)
|
||||
local after = state.text:sub(state.cursor + 1)
|
||||
state.text = before .. textInput .. after
|
||||
state.cursor = state.cursor + #textInput
|
||||
context.input.textInput = ""
|
||||
end
|
||||
if context.input:isKeyDown("backspace") and state.cursor > 0 then
|
||||
deleteRange(state.cursor, 1)
|
||||
state.cursor = state.cursor - 1
|
||||
context.input.keys.backspace = false
|
||||
end
|
||||
if context.input:isKeyDown("delete") and state.cursor < #state.text then
|
||||
deleteRange(state.cursor + 1, 1)
|
||||
context.input.keys.delete = false
|
||||
end
|
||||
if context.input:isKeyDown("left") and state.cursor > 0 then
|
||||
state.cursor = state.cursor - 1
|
||||
context.input.keys.left = false
|
||||
end
|
||||
if context.input:isKeyDown("right") and state.cursor < #state.text then
|
||||
state.cursor = state.cursor + 1
|
||||
context.input.keys.right = false
|
||||
end
|
||||
if context.input:isKeyDown("home") then
|
||||
state.cursor = 0
|
||||
context.input.keys.home = false
|
||||
end
|
||||
if context.input:isKeyDown("end") then
|
||||
state.cursor = #state.text
|
||||
context.input.keys.end = false
|
||||
end
|
||||
if context.input:isKeyDown("enter") then
|
||||
if opts.onSubmit then
|
||||
opts.onSubmit(state.text)
|
||||
end
|
||||
context.input.keys.enter = false
|
||||
end
|
||||
end
|
||||
state.cursor = clamp(state.cursor, 0, #state.text)
|
||||
if state.cursor < state.offset then
|
||||
state.offset = state.cursor
|
||||
end
|
||||
if state.cursor > state.offset + maxChars then
|
||||
state.offset = state.cursor - maxChars
|
||||
end
|
||||
local offset = clamp(state.offset, 0, math.max(0, #state.text - maxChars))
|
||||
state.offset = offset
|
||||
local visibleText = state.text:sub(offset + 1, offset + maxChars)
|
||||
local display = visibleText
|
||||
if display == "" and opts.placeholder and not focused then
|
||||
context:pushText(rect, {
|
||||
text = opts.placeholder,
|
||||
color = style.placeholder,
|
||||
alignX = "left",
|
||||
alignY = "center",
|
||||
padding = pad,
|
||||
})
|
||||
else
|
||||
context:pushText(rect, {
|
||||
text = display,
|
||||
color = style.text,
|
||||
alignX = "left",
|
||||
alignY = "center",
|
||||
padding = pad,
|
||||
clip = true,
|
||||
})
|
||||
end
|
||||
if focused then
|
||||
context:pushRect({
|
||||
x = rect.x + pad + (state.cursor - offset) * charWidth - 1,
|
||||
y = rect.y + pad / 2,
|
||||
width = opts.caretWidth or 2,
|
||||
height = rect.height - pad,
|
||||
}, {
|
||||
color = style.cursor,
|
||||
})
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
function Gui.listView(context, widgetId, rectDef, items, state, opts)
|
||||
opts = opts or {}
|
||||
state = state or {}
|
||||
items = items or {}
|
||||
state.scroll = state.scroll or 0
|
||||
local itemCount = #items
|
||||
if itemCount == 0 then
|
||||
state.selectedIndex = 0
|
||||
else
|
||||
state.selectedIndex = clamp(state.selectedIndex or 1, 1, itemCount)
|
||||
end
|
||||
local rect = ensureRect(rectDef)
|
||||
local style = context.style.listview
|
||||
local hovered, clicked = context:processInteraction(widgetId, rect)
|
||||
local focus = context.focusWidget == widgetId
|
||||
if clicked then
|
||||
context:requestFocus(widgetId)
|
||||
end
|
||||
context:pushRect(rect, {
|
||||
color = style.background,
|
||||
borderColor = style.border,
|
||||
})
|
||||
local itemHeight = opts.itemHeight or (context.style.fontSize + context.style.spacing)
|
||||
local contentHeight = #items * itemHeight
|
||||
state.scroll = clamp(state.scroll, 0, math.max(0, contentHeight - rect.height))
|
||||
if state.selectedIndex > 0 and opts.scrollToSelection then
|
||||
local scrollTarget = (state.selectedIndex - 1) * itemHeight
|
||||
if scrollTarget < state.scroll then
|
||||
state.scroll = scrollTarget
|
||||
elseif scrollTarget + itemHeight > state.scroll + rect.height then
|
||||
state.scroll = scrollTarget + itemHeight - rect.height
|
||||
end
|
||||
end
|
||||
local scrollDelta = context.input.wheel or 0
|
||||
if (hovered or focus) and scrollDelta ~= 0 then
|
||||
state.scroll = clamp(state.scroll - scrollDelta * (opts.scrollSpeed or 20), 0, math.max(0, contentHeight - rect.height))
|
||||
end
|
||||
context:pushClip(rect)
|
||||
local baseY = rect.y - state.scroll
|
||||
for index, item in ipairs(items) do
|
||||
local rowY = baseY + (index - 1) * itemHeight
|
||||
if rowY + itemHeight >= rect.y and rowY <= rect.y + rect.height then
|
||||
local rowRect = {x = rect.x, y = rowY, width = rect.width, height = itemHeight}
|
||||
local rowColor = style.alternate
|
||||
if index % 2 == 0 then
|
||||
rowColor = style.background
|
||||
end
|
||||
if index == state.selectedIndex then
|
||||
rowColor = style.selection
|
||||
end
|
||||
context:pushRect(rowRect, {color = rowColor})
|
||||
context:pushText(rowRect, {
|
||||
text = (opts.itemFormatter and opts.itemFormatter(item, index)) or tostring(item),
|
||||
color = style.text,
|
||||
alignX = "left",
|
||||
alignY = "center",
|
||||
padding = context.style.spacing,
|
||||
clip = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
context:popClip()
|
||||
if clicked then
|
||||
local relativeY = context.input.mouseY - rect.y + state.scroll
|
||||
local clickedIndex = math.floor(relativeY / itemHeight) + 1
|
||||
if clickedIndex >= 1 and clickedIndex <= #items then
|
||||
state.selectedIndex = clickedIndex
|
||||
if opts.onSelect then
|
||||
opts.onSelect(clickedIndex, items[clickedIndex])
|
||||
end
|
||||
end
|
||||
end
|
||||
if focus then
|
||||
if context.input:isKeyDown("up") and state.selectedIndex > 1 then
|
||||
state.selectedIndex = state.selectedIndex - 1
|
||||
end
|
||||
if context.input:isKeyDown("down") and state.selectedIndex < #items then
|
||||
state.selectedIndex = state.selectedIndex + 1
|
||||
end
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
function Gui.newTextState(initial)
|
||||
return {
|
||||
text = initial or "",
|
||||
cursor = #initial,
|
||||
offset = 0,
|
||||
}
|
||||
end
|
||||
|
||||
function Gui.newListState()
|
||||
return {
|
||||
scroll = 0,
|
||||
selectedIndex = 1,
|
||||
}
|
||||
end
|
||||
|
||||
Gui.style = DEFAULT_STYLE
|
||||
return Gui
|
||||
134
scripts/gui_demo.lua
Normal file
134
scripts/gui_demo.lua
Normal file
@@ -0,0 +1,134 @@
|
||||
local Gui = require(\ gui\)
|
||||
local math3d = require(\math3d\)
|
||||
|
||||
local ctx = Gui.newContext()
|
||||
local input = Gui.newInputState()
|
||||
local textState = Gui.newTextState(\\)
|
||||
local listState = Gui.newListState()
|
||||
local items = {
|
||||
\Dashboard Setup\,
|
||||
\Input Streams\,
|
||||
\Telemetry\,
|
||||
\Power Profile\,
|
||||
\Diagnostics\,
|
||||
\Release Notes\,
|
||||
}
|
||||
local statusMessage = \Idle\
|
||||
local selectedItem = items[1]
|
||||
local rotationSpeeds = {x = 0.45, y = 0.65}
|
||||
|
||||
local cubeVertices = {
|
||||
{ position = {-1.0, -1.0, -1.0}, color = {1.0, 0.2, 0.4} },
|
||||
{ position = {1.0, -1.0, -1.0}, color = {0.2, 0.9, 0.4} },
|
||||
{ position = {1.0, 1.0, -1.0}, color = {0.3, 0.4, 0.9} },
|
||||
{ position = {-1.0, 1.0, -1.0}, color = {1.0, 0.8, 0.2} },
|
||||
{ position = {-1.0, -1.0, 1.0}, color = {0.9, 0.4, 0.5} },
|
||||
{ position = {1.0, -1.0, 1.0}, color = {0.4, 0.9, 0.5} },
|
||||
{ position = {1.0, 1.0, 1.0}, color = {0.6, 0.8, 1.0} },
|
||||
{ position = {-1.0, 1.0, 1.0}, color = {0.4, 0.4, 0.4} },
|
||||
}
|
||||
|
||||
local cubeIndices = {
|
||||
1, 2, 3, 3, 4, 1,
|
||||
5, 6, 7, 7, 8, 5,
|
||||
1, 5, 8, 8, 4, 1,
|
||||
2, 6, 7, 7, 3, 2,
|
||||
4, 3, 7, 7, 8, 4,
|
||||
1, 2, 6, 6, 5, 1,
|
||||
}
|
||||
|
||||
local shaderVariants = {
|
||||
default = {
|
||||
vertex = \shaders/cube.vert.spv\,
|
||||
fragment = \shaders/cube.frag.spv\,
|
||||
},
|
||||
}
|
||||
|
||||
local camera = {
|
||||
eye = {2.0, 2.0, 3.0},
|
||||
center = {0.0, 0.0, 0.0},
|
||||
up = {0.0, 1.0, 0.0},
|
||||
fov = 0.78,
|
||||
near = 0.1,
|
||||
far = 10.0,
|
||||
}
|
||||
|
||||
local function buildModel(time)
|
||||
local y = math3d.rotation_y(time * rotationSpeeds.y)
|
||||
local x = math3d.rotation_x(time * rotationSpeeds.x)
|
||||
return math3d.multiply(y, x)
|
||||
end
|
||||
|
||||
local function createCube(position)
|
||||
local function computeModel(time)
|
||||
local base = buildModel(time)
|
||||
local offset = math3d.translation(position[1], position[2], position[3])
|
||||
return math3d.multiply(offset, base)
|
||||
end
|
||||
return {
|
||||
vertices = cubeVertices,
|
||||
indices = cubeIndices,
|
||||
compute_model_matrix = computeModel,
|
||||
shader_key = \default\,
|
||||
}
|
||||
end
|
||||
|
||||
gui_context = ctx
|
||||
gui_input = input
|
||||
|
||||
function get_scene_objects()
|
||||
return { createCube({0.0, 0.0, -4.0}) }
|
||||
end
|
||||
|
||||
function get_shader_paths()
|
||||
return shaderVariants
|
||||
end
|
||||
|
||||
function get_view_projection(aspect)
|
||||
local view = math3d.look_at(camera.eye, camera.center, camera.up)
|
||||
local projection = math3d.perspective(camera.fov, aspect, camera.near, camera.far)
|
||||
return math3d.multiply(projection, view)
|
||||
end
|
||||
|
||||
local function drawPanel()
|
||||
ctx:pushRect({x = 10, y = 10, width = 460, height = 520}, {
|
||||
color = {0.06, 0.07, 0.09, 0.9},
|
||||
borderColor = {0.35, 0.38, 0.42, 1.0},
|
||||
})
|
||||
Gui.text(ctx, {x = 30, y = 30, width = 420, height = 30}, \Command Console\, {
|
||||
fontSize = 24,
|
||||
alignX = \left\,
|
||||
color = {0.95, 0.95, 0.95, 1.0},
|
||||
})
|
||||
Gui.svg(ctx, {x = 320, y = 30, width = 120, height = 120}, \assets/logo.svg\)
|
||||
textState = Gui.textbox(ctx, \search_field\, {x = 30, y = 80, width = 420, height = 40}, textState, {
|
||||
placeholder = \Filter modules...\,
|
||||
onSubmit = function(text)
|
||||
statusMessage = \Searching for: \ .. (text ~= \\ and text or \anything\)
|
||||
end,
|
||||
})
|
||||
|
||||
listState = Gui.listView(ctx, \menu_list\, {x = 30, y = 140, width = 420, height = 240}, items, listState, {
|
||||
onSelect = function(idx, item)
|
||||
selectedItem = item
|
||||
statusMessage = \Ready to adjust \ .. item
|
||||
end,
|
||||
scrollToSelection = true,
|
||||
})
|
||||
|
||||
if Gui.button(ctx, \apply\, {x = 30, y = 400, width = 200, height = 38}, \Apply Settings\) then
|
||||
statusMessage = \Applied configuration for \ .. (selectedItem or \–\)
|
||||
end
|
||||
|
||||
Gui.text(ctx, {x = 30, y = 448, width = 420, height = 24}, \Status: \ .. statusMessage, {
|
||||
color = {0.6, 0.9, 1.0, 1.0},
|
||||
alignY = \top\,
|
||||
})
|
||||
end
|
||||
|
||||
function get_gui_commands()
|
||||
ctx:beginFrame(input)
|
||||
drawPanel()
|
||||
ctx:endFrame()
|
||||
return ctx:getCommands()
|
||||
end
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
#include "core/vertex.hpp"
|
||||
#include "script/cube_script.hpp"
|
||||
#include "gui/gui_renderer.hpp"
|
||||
|
||||
namespace sdl3cpp::app {
|
||||
|
||||
@@ -87,6 +89,8 @@ private:
|
||||
const std::array<float, 16>& viewProj);
|
||||
void CreateSyncObjects();
|
||||
void DrawFrame(float time);
|
||||
void SetupGuiRenderer();
|
||||
void ProcessGuiEvent(const SDL_Event& event);
|
||||
|
||||
QueueFamilyIndices FindQueueFamilies(VkPhysicalDevice device);
|
||||
bool CheckDeviceExtensionSupport(VkPhysicalDevice device);
|
||||
@@ -126,6 +130,10 @@ private:
|
||||
std::string defaultShaderKey_;
|
||||
VkFence inFlightFence_ = VK_NULL_HANDLE;
|
||||
bool framebufferResized_ = false;
|
||||
script::GuiInputSnapshot guiInputSnapshot_;
|
||||
std::vector<script::CubeScript::GuiCommand> guiCommands_;
|
||||
std::unique_ptr<gui::GuiRenderer> guiRenderer_;
|
||||
bool guiHasCommands_ = false;
|
||||
std::vector<RenderObject> renderObjects_;
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ void Sdl3App::InitSDL() {
|
||||
if (!window_) {
|
||||
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
|
||||
}
|
||||
SDL_StartTextInput();
|
||||
}
|
||||
|
||||
void Sdl3App::InitVulkan() {
|
||||
@@ -44,6 +45,7 @@ void Sdl3App::InitVulkan() {
|
||||
PickPhysicalDevice();
|
||||
CreateLogicalDevice();
|
||||
CreateSwapChain();
|
||||
SetupGuiRenderer();
|
||||
CreateImageViews();
|
||||
CreateRenderPass();
|
||||
LoadSceneData();
|
||||
@@ -66,9 +68,26 @@ void Sdl3App::MainLoop() {
|
||||
running = false;
|
||||
} else if (event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
|
||||
framebufferResized_ = true;
|
||||
} else if (guiHasCommands_) {
|
||||
ProcessGuiEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
if (guiHasCommands_) {
|
||||
int mouseX = 0;
|
||||
int mouseY = 0;
|
||||
SDL_GetMouseState(&mouseX, &mouseY);
|
||||
guiInputSnapshot_.mouseX = static_cast<float>(mouseX);
|
||||
guiInputSnapshot_.mouseY = static_cast<float>(mouseY);
|
||||
cubeScript_.UpdateGuiInput(guiInputSnapshot_);
|
||||
if (guiRenderer_) {
|
||||
guiCommands_ = cubeScript_.LoadGuiCommands();
|
||||
guiRenderer_->Prepare(guiCommands_, swapChainExtent_.width, swapChainExtent_.height);
|
||||
}
|
||||
guiInputSnapshot_.wheel = 0.0f;
|
||||
guiInputSnapshot_.textInput.clear();
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
float time = std::chrono::duration<float>(now - start).count();
|
||||
DrawFrame(time);
|
||||
@@ -97,6 +116,7 @@ void Sdl3App::Cleanup() {
|
||||
window_ = nullptr;
|
||||
}
|
||||
SDL_Vulkan_UnloadLibrary();
|
||||
SDL_StopTextInput();
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
const std::unordered_map<SDL_Keycode, std::string> kGuiKeyNames = {
|
||||
{SDLK_BACKSPACE, "backspace"},
|
||||
{SDLK_DELETE, "delete"},
|
||||
{SDLK_LEFT, "left"},
|
||||
{SDLK_RIGHT, "right"},
|
||||
{SDLK_HOME, "home"},
|
||||
{SDLK_END, "end"},
|
||||
{SDLK_RETURN, "enter"},
|
||||
{SDLK_UP, "up"},
|
||||
{SDLK_DOWN, "down"},
|
||||
};
|
||||
} // namespace
|
||||
|
||||
namespace sdl3cpp::app {
|
||||
|
||||
@@ -60,9 +75,57 @@ void Sdl3App::RecordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageI
|
||||
vkCmdDrawIndexed(commandBuffer, object.indexCount, 1, object.indexOffset, object.vertexOffset, 0);
|
||||
}
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
if (guiRenderer_) {
|
||||
guiRenderer_->BlitToSwapchain(commandBuffer, swapChainImages_[imageIndex]);
|
||||
}
|
||||
vkEndCommandBuffer(commandBuffer);
|
||||
}
|
||||
|
||||
void Sdl3App::ProcessGuiEvent(const SDL_Event& event) {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
guiInputSnapshot_.mouseX = static_cast<float>(event.motion.x);
|
||||
guiInputSnapshot_.mouseY = static_cast<float>(event.motion.y);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
if (event.button.button == SDL_BUTTON_LEFT) {
|
||||
guiInputSnapshot_.mouseDown = (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN);
|
||||
}
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_WHEEL:
|
||||
guiInputSnapshot_.wheel += static_cast<float>(event.wheel.y);
|
||||
break;
|
||||
case SDL_EVENT_TEXT_INPUT:
|
||||
guiInputSnapshot_.textInput.append(event.text.text);
|
||||
break;
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP: {
|
||||
auto it = kGuiKeyNames.find(event.key.keysym.sym);
|
||||
if (it != kGuiKeyNames.end()) {
|
||||
guiInputSnapshot_.keyStates[it->second] = (event.type == SDL_EVENT_KEY_DOWN);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl3App::SetupGuiRenderer() {
|
||||
guiHasCommands_ = cubeScript_.HasGuiCommands();
|
||||
if (!guiHasCommands_) {
|
||||
guiRenderer_.reset();
|
||||
return;
|
||||
}
|
||||
if (!guiRenderer_) {
|
||||
guiRenderer_ =
|
||||
std::make_unique<gui::GuiRenderer>(device_, physicalDevice_, swapChainImageFormat_,
|
||||
cubeScript_.GetScriptDirectory());
|
||||
}
|
||||
guiRenderer_->Resize(swapChainExtent_.width, swapChainExtent_.height, swapChainImageFormat_);
|
||||
}
|
||||
|
||||
void Sdl3App::DrawFrame(float time) {
|
||||
vkWaitForFences(device_, 1, &inFlightFence_, VK_TRUE, std::numeric_limits<uint64_t>::max());
|
||||
vkResetFences(device_, 1, &inFlightFence_);
|
||||
|
||||
@@ -151,6 +151,7 @@ void Sdl3App::RecreateSwapChain() {
|
||||
CleanupSwapChain();
|
||||
CreateSwapChain();
|
||||
CreateImageViews();
|
||||
SetupGuiRenderer();
|
||||
CreateRenderPass();
|
||||
CreateGraphicsPipeline();
|
||||
CreateFramebuffers();
|
||||
|
||||
565
src/gui/gui_renderer.cpp
Normal file
565
src/gui/gui_renderer.cpp
Normal file
@@ -0,0 +1,565 @@
|
||||
|
||||
#include "gui/gui_renderer.hpp"
|
||||
|
||||
#include "app/vulkan_api.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "../../third_party/font8x8_basic.h"
|
||||
|
||||
namespace script = sdl3cpp::script;
|
||||
|
||||
namespace {
|
||||
|
||||
bool ExtractAttribute(const std::string& source, const char* name, std::string& outValue) {
|
||||
std::string key = name;
|
||||
size_t pos = source.find(key);
|
||||
while (pos != std::string::npos) {
|
||||
size_t eq = source.find('=', pos + key.size());
|
||||
if (eq == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
size_t valueStart = eq + 1;
|
||||
while (valueStart < source.size() &&
|
||||
std::isspace(static_cast<unsigned char>(source[valueStart]))) {
|
||||
valueStart++;
|
||||
}
|
||||
if (valueStart >= source.size()) {
|
||||
break;
|
||||
}
|
||||
char quote = source[valueStart];
|
||||
if (quote != '\"' && quote != '\'') {
|
||||
break;
|
||||
}
|
||||
size_t valueEnd = source.find(quote, valueStart + 1);
|
||||
if (valueEnd == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
outValue = source.substr(valueStart + 1, valueEnd - valueStart - 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
float ParseFloatValue(const std::string& text) {
|
||||
try {
|
||||
size_t idx = 0;
|
||||
return std::stof(text, &idx);
|
||||
} catch (...) {
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
script::GuiColor ParseColorString(const std::string& text, const script::GuiColor& fallback) {
|
||||
if (text.empty() || text[0] != '#') {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
if (text.size() == 7) {
|
||||
unsigned int rgb = std::stoul(text.substr(1), nullptr, 16);
|
||||
return {((rgb >> 16) & 0xFF) / 255.0f, ((rgb >> 8) & 0xFF) / 255.0f,
|
||||
(rgb & 0xFF) / 255.0f, 1.0f};
|
||||
}
|
||||
if (text.size() == 9) {
|
||||
unsigned int rgba = std::stoul(text.substr(1), nullptr, 16);
|
||||
return {((rgba >> 24) & 0xFF) / 255.0f, ((rgba >> 16) & 0xFF) / 255.0f,
|
||||
((rgba >> 8) & 0xFF) / 255.0f, (rgba & 0xFF) / 255.0f};
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
ParsedSvg ParseSvgFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Failed to open SVG file: " + path.string());
|
||||
}
|
||||
std::string data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
||||
ParsedSvg result;
|
||||
std::string value;
|
||||
if (ExtractAttribute(data, "viewBox", value)) {
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
std::sscanf(value.c_str(), "%f %f %f %f", &x, &y, &w, &h);
|
||||
if (w > 0.0f && h > 0.0f) {
|
||||
result.viewWidth = w;
|
||||
result.viewHeight = h;
|
||||
}
|
||||
}
|
||||
if (ExtractAttribute(data, "width", value)) {
|
||||
result.viewWidth = ParseFloatValue(value);
|
||||
}
|
||||
if (ExtractAttribute(data, "height", value)) {
|
||||
result.viewHeight = ParseFloatValue(value);
|
||||
}
|
||||
if (result.viewWidth <= 0.0f) {
|
||||
result.viewWidth = 128.0f;
|
||||
}
|
||||
if (result.viewHeight <= 0.0f) {
|
||||
result.viewHeight = 128.0f;
|
||||
}
|
||||
|
||||
size_t search = 0;
|
||||
while (true) {
|
||||
size_t tagStart = data.find("<circle", search);
|
||||
if (tagStart == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
size_t tagEnd = data.find('>', tagStart);
|
||||
if (tagEnd == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
std::string tag = data.substr(tagStart, tagEnd - tagStart);
|
||||
SvgCircle circle;
|
||||
std::string attr;
|
||||
if (ExtractAttribute(tag, "cx", attr)) {
|
||||
circle.cx = ParseFloatValue(attr);
|
||||
}
|
||||
if (ExtractAttribute(tag, "cy", attr)) {
|
||||
circle.cy = ParseFloatValue(attr);
|
||||
}
|
||||
if (ExtractAttribute(tag, "r", attr)) {
|
||||
circle.r = ParseFloatValue(attr);
|
||||
}
|
||||
if (ExtractAttribute(tag, "fill", attr)) {
|
||||
circle.color = ParseColorString(attr, circle.color);
|
||||
}
|
||||
result.circles.push_back(circle);
|
||||
search = tagEnd + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
script::CubeScript::GuiCommand::RectData IntersectRect(const script::CubeScript::GuiCommand::RectData& a,
|
||||
const script::CubeScript::GuiCommand::RectData& b) {
|
||||
script::CubeScript::GuiCommand::RectData result;
|
||||
result.x = std::max(a.x, b.x);
|
||||
result.y = std::max(a.y, b.y);
|
||||
float right = std::min(a.x + a.width, b.x + b.width);
|
||||
float bottom = std::min(a.y + a.height, b.y + b.height);
|
||||
result.width = std::max(0.0f, right - result.x);
|
||||
result.height = std::max(0.0f, bottom - result.y);
|
||||
return result;
|
||||
}
|
||||
|
||||
int ClampToRange(int value, int minimum, int maximum) {
|
||||
return std::min(std::max(value, minimum), maximum);
|
||||
}
|
||||
|
||||
class Canvas {
|
||||
public:
|
||||
using RectData = script::CubeScript::GuiCommand::RectData;
|
||||
|
||||
void Resize(uint32_t width, uint32_t height) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
pixels_.assign(static_cast<size_t>(width_) * static_cast<size_t>(height_) * 4, 0);
|
||||
clipStack_.clear();
|
||||
clipStack_.push_back({0.0f, 0.0f, static_cast<float>(width_), static_cast<float>(height_)});
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
std::fill(pixels_.begin(), pixels_.end(), 0);
|
||||
clipStack_.clear();
|
||||
clipStack_.push_back({0.0f, 0.0f, static_cast<float>(width_), static_cast<float>(height_)});
|
||||
}
|
||||
|
||||
void PushClip(const RectData& rect) {
|
||||
clipStack_.push_back(rect);
|
||||
}
|
||||
|
||||
void PopClip() {
|
||||
if (clipStack_.size() > 1) {
|
||||
clipStack_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void FillRect(const RectData& rect, const script::GuiColor& fillColor,
|
||||
const script::GuiColor& borderColor, float borderWidth) {
|
||||
DrawFilledRect(rect, fillColor);
|
||||
if (borderWidth > 0.0f && borderColor.a > 0.0f) {
|
||||
DrawFilledRect({rect.x, rect.y, rect.width, borderWidth}, borderColor);
|
||||
DrawFilledRect({rect.x, rect.y + rect.height - borderWidth, rect.width, borderWidth}, borderColor);
|
||||
DrawFilledRect({rect.x, rect.y + borderWidth, borderWidth, rect.height - borderWidth * 2.0f}, borderColor);
|
||||
DrawFilledRect({rect.x + rect.width - borderWidth, rect.y + borderWidth, borderWidth,
|
||||
rect.height - borderWidth * 2.0f}, borderColor);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawText(const std::string& text, const script::GuiColor& color, const RectData& bounds,
|
||||
const std::string& alignX, const std::string& alignY, float fontSize) {
|
||||
if (text.empty() || width_ == 0 || height_ == 0) {
|
||||
return;
|
||||
}
|
||||
float scale = std::max(1.0f, fontSize / 8.0f);
|
||||
float glyphWidth = 8.0f * scale;
|
||||
float glyphHeight = 8.0f * scale;
|
||||
float textWidth = glyphWidth * static_cast<float>(text.size());
|
||||
float x = bounds.x;
|
||||
if (alignX == "center") {
|
||||
x += (bounds.width - textWidth) * 0.5f;
|
||||
} else if (alignX == "right") {
|
||||
x += bounds.width - textWidth;
|
||||
}
|
||||
float y = bounds.y;
|
||||
if (alignY == "center") {
|
||||
y += (bounds.height - glyphHeight) * 0.5f;
|
||||
} else if (alignY == "bottom") {
|
||||
y += bounds.height - glyphHeight;
|
||||
}
|
||||
for (size_t i = 0; i < text.size(); ++i) {
|
||||
unsigned char code = static_cast<unsigned char>(text[i]);
|
||||
if (code >= 128) {
|
||||
continue;
|
||||
}
|
||||
float glyphX = x + glyphWidth * static_cast<float>(i);
|
||||
for (int row = 0; row < 8; ++row) {
|
||||
uint8_t pattern = static_cast<uint8_t>(font8x8_basic[code][row]);
|
||||
for (int col = 0; col < 8; ++col) {
|
||||
if ((pattern & (1 << col)) == 0) {
|
||||
continue;
|
||||
}
|
||||
RectData pixelRect{
|
||||
glyphX + static_cast<float>(col) * scale,
|
||||
y + static_cast<float>(row) * scale,
|
||||
scale,
|
||||
scale,
|
||||
};
|
||||
DrawFilledRect(pixelRect, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawSvg(const ParsedSvg& svg, const RectData& target, const script::GuiColor& tint) {
|
||||
if (svg.circles.empty() || svg.viewWidth <= 0.0f || svg.viewHeight <= 0.0f || width_ == 0 ||
|
||||
height_ == 0) {
|
||||
return;
|
||||
}
|
||||
RectData clipped = ClipRect(target);
|
||||
if (clipped.width <= 0.0f || clipped.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
float scaleX = clipped.width / svg.viewWidth;
|
||||
float scaleY = clipped.height / svg.viewHeight;
|
||||
float scale = std::min(scaleX, scaleY);
|
||||
for (const auto& circle : svg.circles) {
|
||||
float cx = clipped.x + circle.cx * scaleX;
|
||||
float cy = clipped.y + circle.cy * scaleY;
|
||||
float radius = circle.r * scale;
|
||||
script::GuiColor color = circle.color;
|
||||
if (tint.a > 0.0f) {
|
||||
color.r *= tint.r;
|
||||
color.g *= tint.g;
|
||||
color.b *= tint.b;
|
||||
color.a *= tint.a;
|
||||
}
|
||||
int yStart = ClampToRange(static_cast<int>(std::floor(cy - radius)), 0, static_cast<int>(height_));
|
||||
int yEnd = ClampToRange(static_cast<int>(std::ceil(cy + radius)), 0, static_cast<int>(height_));
|
||||
for (int row = yStart; row < yEnd; ++row) {
|
||||
float dy = (static_cast<float>(row) + 0.5f) - cy;
|
||||
float horizontalSpan = radius * radius - dy * dy;
|
||||
if (horizontalSpan <= 0.0f) {
|
||||
continue;
|
||||
}
|
||||
float span = std::sqrt(horizontalSpan);
|
||||
RectData slice{
|
||||
cx - span,
|
||||
static_cast<float>(row),
|
||||
2.0f * span,
|
||||
1.0f,
|
||||
};
|
||||
DrawFilledRect(slice, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<uint8_t>& Pixels() const {
|
||||
return pixels_;
|
||||
}
|
||||
|
||||
private:
|
||||
RectData ClipRect(const RectData& rect) const {
|
||||
RectData clipped = rect;
|
||||
for (const auto& entry : clipStack_) {
|
||||
clipped = IntersectRect(clipped, entry);
|
||||
}
|
||||
return clipped;
|
||||
}
|
||||
|
||||
void DrawFilledRect(const RectData& rect, const script::GuiColor& color) {
|
||||
if (rect.width <= 0.0f || rect.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
RectData clipped = ClipRect(rect);
|
||||
if (clipped.width <= 0.0f || clipped.height <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
int startX = ClampToRange(static_cast<int>(std::floor(clipped.x)), 0, static_cast<int>(width_));
|
||||
int startY = ClampToRange(static_cast<int>(std::floor(clipped.y)), 0, static_cast<int>(height_));
|
||||
int endX = ClampToRange(static_cast<int>(std::ceil(clipped.x + clipped.width)), 0, static_cast<int>(width_));
|
||||
int endY = ClampToRange(static_cast<int>(std::ceil(clipped.y + clipped.height)), 0, static_cast<int>(height_));
|
||||
for (int y = startY; y < endY; ++y) {
|
||||
for (int x = startX; x < endX; ++x) {
|
||||
BlendPixel(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlendPixel(int x, int y, const script::GuiColor& color) {
|
||||
size_t index = (static_cast<size_t>(y) * width_ + static_cast<size_t>(x)) * 4;
|
||||
auto clampByte = [](float value) -> uint8_t {
|
||||
return static_cast<uint8_t>(std::clamp(value, 0.0f, 1.0f) * 255.0f);
|
||||
};
|
||||
float destR = pixels_[index] / 255.0f;
|
||||
float destG = pixels_[index + 1] / 255.0f;
|
||||
float destB = pixels_[index + 2] / 255.0f;
|
||||
float destA = pixels_[index + 3] / 255.0f;
|
||||
float srcA = std::clamp(color.a, 0.0f, 1.0f);
|
||||
float invSrc = 1.0f - srcA;
|
||||
float outR = color.r * srcA + destR * invSrc;
|
||||
float outG = color.g * srcA + destG * invSrc;
|
||||
float outB = color.b * srcA + destB * invSrc;
|
||||
float outA = srcA + destA * invSrc;
|
||||
pixels_[index] = clampByte(outR);
|
||||
pixels_[index + 1] = clampByte(outG);
|
||||
pixels_[index + 2] = clampByte(outB);
|
||||
pixels_[index + 3] = clampByte(outA);
|
||||
}
|
||||
|
||||
uint32_t width_ = 0;
|
||||
uint32_t height_ = 0;
|
||||
std::vector<uint8_t> pixels_;
|
||||
std::vector<RectData> clipStack_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace sdl3cpp::gui {
|
||||
GuiRenderer::GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
|
||||
const std::filesystem::path& scriptDirectory)
|
||||
: device_(device),
|
||||
physicalDevice_(physicalDevice),
|
||||
swapchainFormat_(swapchainFormat),
|
||||
scriptDirectory_(scriptDirectory),
|
||||
canvas_(std::make_unique<Canvas>()) {}
|
||||
|
||||
GuiRenderer::~GuiRenderer() {
|
||||
DestroyStagingBuffer();
|
||||
}
|
||||
|
||||
bool GuiRenderer::IsReady() const {
|
||||
return canvasWidth_ > 0 && canvasHeight_ > 0 && stagingBuffer_ != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
void GuiRenderer::Prepare(const std::vector<script::CubeScript::GuiCommand>& commands, uint32_t width,
|
||||
uint32_t height) {
|
||||
if (width == 0 || height == 0 || !canvas_) {
|
||||
return;
|
||||
}
|
||||
EnsureCanvas(width, height);
|
||||
canvas_->Clear();
|
||||
for (const auto& command : commands) {
|
||||
switch (command.type) {
|
||||
case script::CubeScript::GuiCommand::Type::Rect:
|
||||
canvas_->FillRect(command.rect, command.color, command.borderColor, command.borderWidth);
|
||||
break;
|
||||
case script::CubeScript::GuiCommand::Type::Text: {
|
||||
if (command.hasClipRect) {
|
||||
canvas_->PushClip(command.clipRect);
|
||||
}
|
||||
if (command.hasBounds) {
|
||||
canvas_->DrawText(command.text, command.color, command.bounds, command.alignX,
|
||||
command.alignY, command.fontSize);
|
||||
} else {
|
||||
script::CubeScript::GuiCommand::RectData fallback{
|
||||
command.rect.x, command.rect.y,
|
||||
command.fontSize * static_cast<float>(std::max<size_t>(1, command.text.size())), command.fontSize};
|
||||
canvas_->DrawText(command.text, command.color, fallback, command.alignX,
|
||||
command.alignY, command.fontSize);
|
||||
}
|
||||
if (command.hasClipRect) {
|
||||
canvas_->PopClip();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case script::CubeScript::GuiCommand::Type::ClipPush:
|
||||
canvas_->PushClip(command.rect);
|
||||
break;
|
||||
case script::CubeScript::GuiCommand::Type::ClipPop:
|
||||
canvas_->PopClip();
|
||||
break;
|
||||
case script::CubeScript::GuiCommand::Type::Svg:
|
||||
if (command.svgPath.empty()) {
|
||||
break;
|
||||
}
|
||||
if (const ParsedSvg* svg = LoadSvg(command.svgPath)) {
|
||||
canvas_->DrawSvg(*svg, command.rect, command.svgTint);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
UpdateStagingBuffer();
|
||||
}
|
||||
|
||||
void GuiRenderer::BlitToSwapchain(VkCommandBuffer commandBuffer, VkImage image) {
|
||||
if (!IsReady()) {
|
||||
return;
|
||||
}
|
||||
VkImageMemoryBarrier barrier{};
|
||||
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.image = image;
|
||||
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
barrier.subresourceRange.baseMipLevel = 0;
|
||||
barrier.subresourceRange.levelCount = 1;
|
||||
barrier.subresourceRange.baseArrayLayer = 0;
|
||||
barrier.subresourceRange.layerCount = 1;
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
VkBufferImageCopy region{};
|
||||
region.bufferOffset = 0;
|
||||
region.bufferRowLength = 0;
|
||||
region.bufferImageHeight = 0;
|
||||
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
region.imageSubresource.mipLevel = 0;
|
||||
region.imageSubresource.baseArrayLayer = 0;
|
||||
region.imageSubresource.layerCount = 1;
|
||||
region.imageOffset = {0, 0, 0};
|
||||
region.imageExtent = {canvasWidth_, canvasHeight_, 1};
|
||||
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer_, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
|
||||
®ion);
|
||||
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.dstAccessMask = 0;
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0,
|
||||
0, nullptr, 0, nullptr, 1, &barrier);
|
||||
}
|
||||
|
||||
void GuiRenderer::Resize(uint32_t width, uint32_t height, VkFormat format) {
|
||||
if (width == canvasWidth_ && height == canvasHeight_ && format == swapchainFormat_) {
|
||||
return;
|
||||
}
|
||||
UpdateFormat(format);
|
||||
EnsureCanvas(width, height);
|
||||
}
|
||||
|
||||
void GuiRenderer::EnsureCanvas(uint32_t width, uint32_t height) {
|
||||
if (width == canvasWidth_ && height == canvasHeight_) {
|
||||
return;
|
||||
}
|
||||
canvasWidth_ = width;
|
||||
canvasHeight_ = height;
|
||||
if (canvas_) {
|
||||
canvas_->Resize(width, height);
|
||||
}
|
||||
size_t bufferSize = static_cast<size_t>(canvasWidth_) * canvasHeight_ * 4;
|
||||
CreateStagingBuffer(bufferSize);
|
||||
}
|
||||
|
||||
void GuiRenderer::UpdateStagingBuffer() {
|
||||
if (!stagingMapped_ || !canvas_) {
|
||||
return;
|
||||
}
|
||||
const auto& pixels = canvas_->Pixels();
|
||||
size_t pixelCount = static_cast<size_t>(canvasWidth_) * canvasHeight_;
|
||||
uint8_t* dest = reinterpret_cast<uint8_t*>(stagingMapped_);
|
||||
for (size_t i = 0; i < pixelCount; ++i) {
|
||||
size_t offset = i * 4;
|
||||
uint8_t r = pixels[offset];
|
||||
uint8_t g = pixels[offset + 1];
|
||||
uint8_t b = pixels[offset + 2];
|
||||
uint8_t a = pixels[offset + 3];
|
||||
switch (swapchainFormat_) {
|
||||
case VK_FORMAT_B8G8R8A8_UNORM:
|
||||
case VK_FORMAT_B8G8R8A8_SRGB:
|
||||
dest[offset] = b;
|
||||
dest[offset + 1] = g;
|
||||
dest[offset + 2] = r;
|
||||
dest[offset + 3] = a;
|
||||
break;
|
||||
case VK_FORMAT_R8G8B8A8_UNORM:
|
||||
case VK_FORMAT_R8G8B8A8_SRGB:
|
||||
default:
|
||||
dest[offset] = r;
|
||||
dest[offset + 1] = g;
|
||||
dest[offset + 2] = b;
|
||||
dest[offset + 3] = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRenderer::CreateStagingBuffer(size_t size) {
|
||||
DestroyStagingBuffer();
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
vulkan::CreateBuffer(device_, physicalDevice_, static_cast<VkDeviceSize>(size),
|
||||
VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
|
||||
stagingBuffer_, stagingMemory_);
|
||||
stagingSize_ = size;
|
||||
vkMapMemory(device_, stagingMemory_, 0, stagingSize_, 0, &stagingMapped_);
|
||||
}
|
||||
|
||||
void GuiRenderer::DestroyStagingBuffer() {
|
||||
if (stagingMapped_) {
|
||||
vkUnmapMemory(device_, stagingMemory_);
|
||||
stagingMapped_ = nullptr;
|
||||
}
|
||||
if (stagingBuffer_ != VK_NULL_HANDLE) {
|
||||
vkDestroyBuffer(device_, stagingBuffer_, nullptr);
|
||||
stagingBuffer_ = VK_NULL_HANDLE;
|
||||
}
|
||||
if (stagingMemory_ != VK_NULL_HANDLE) {
|
||||
vkFreeMemory(device_, stagingMemory_, nullptr);
|
||||
stagingMemory_ = VK_NULL_HANDLE;
|
||||
}
|
||||
stagingSize_ = 0;
|
||||
}
|
||||
|
||||
void GuiRenderer::UpdateFormat(VkFormat format) {
|
||||
if (swapchainFormat_ == format) {
|
||||
return;
|
||||
}
|
||||
swapchainFormat_ = format;
|
||||
DestroyStagingBuffer();
|
||||
if (canvasWidth_ > 0 && canvasHeight_ > 0) {
|
||||
size_t bufferSize = static_cast<size_t>(canvasWidth_) * canvasHeight_ * 4;
|
||||
CreateStagingBuffer(bufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
const ParsedSvg* GuiRenderer::LoadSvg(const std::string& relativePath) {
|
||||
auto it = svgCache_.find(relativePath);
|
||||
if (it != svgCache_.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
std::filesystem::path path = scriptDirectory_ / relativePath;
|
||||
try {
|
||||
ParsedSvg parsed = ParseSvgFile(path);
|
||||
auto inserted = svgCache_.emplace(relativePath, std::move(parsed));
|
||||
return &inserted.first->second;
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::gui
|
||||
69
src/gui/gui_renderer.hpp
Normal file
69
src/gui/gui_renderer.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
#ifndef SDL3CPP_GUI_GUI_RENDERER_HPP
|
||||
#define SDL3CPP_GUI_GUI_RENDERER_HPP
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include script/cube_script.hpp
|
||||
|
||||
namespace sdl3cpp::gui {
|
||||
|
||||
struct SvgCircle {
|
||||
float cx = 0.0f;
|
||||
float cy = 0.0f;
|
||||
float r = 0.0f;
|
||||
script::GuiColor color{1.0f, 1.0f, 1.0f, 1.0f};
|
||||
};
|
||||
|
||||
struct ParsedSvg {
|
||||
float viewWidth = 1.0f;
|
||||
float viewHeight = 1.0f;
|
||||
std::vector<SvgCircle> circles;
|
||||
};
|
||||
|
||||
class GuiRenderer {
|
||||
public:
|
||||
GuiRenderer(VkDevice device, VkPhysicalDevice physicalDevice, VkFormat swapchainFormat,
|
||||
const std::filesystem::path& scriptDirectory);
|
||||
~GuiRenderer();
|
||||
|
||||
GuiRenderer(const GuiRenderer&) = delete;
|
||||
GuiRenderer& operator=(const GuiRenderer&) = delete;
|
||||
|
||||
void Prepare(const std::vector<script::CubeScript::GuiCommand>& commands, uint32_t width,
|
||||
uint32_t height);
|
||||
void BlitToSwapchain(VkCommandBuffer commandBuffer, VkImage image);
|
||||
void Resize(uint32_t width, uint32_t height, VkFormat format);
|
||||
bool IsReady() const;
|
||||
|
||||
private:
|
||||
struct Canvas;
|
||||
|
||||
const ParsedSvg* LoadSvg(const std::string& relativePath);
|
||||
|
||||
void EnsureCanvas(uint32_t width, uint32_t height);
|
||||
void UpdateStagingBuffer();
|
||||
void CreateStagingBuffer(size_t size);
|
||||
void DestroyStagingBuffer();
|
||||
void UpdateFormat(VkFormat format);
|
||||
|
||||
VkDevice device_;
|
||||
VkPhysicalDevice physicalDevice_;
|
||||
VkFormat swapchainFormat_;
|
||||
std::filesystem::path scriptDirectory_;
|
||||
VkBuffer stagingBuffer_ = VK_NULL_HANDLE;
|
||||
VkDeviceMemory stagingMemory_ = VK_NULL_HANDLE;
|
||||
void* stagingMapped_ = nullptr;
|
||||
size_t stagingSize_ = 0;
|
||||
uint32_t canvasWidth_ = 0;
|
||||
uint32_t canvasHeight_ = 0;
|
||||
std::unique_ptr<Canvas> canvas_;
|
||||
std::unordered_map<std::string, ParsedSvg> svgCache_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::gui
|
||||
|
||||
#endif // SDL3CPP_GUI_GUI_RENDERER_HPP
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "script/cube_script.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -17,7 +18,8 @@ std::array<float, 16> IdentityMatrix() {
|
||||
|
||||
} // namespace
|
||||
|
||||
CubeScript::CubeScript(const std::filesystem::path& scriptPath) : L_(luaL_newstate()) {
|
||||
CubeScript::CubeScript(const std::filesystem::path& scriptPath)
|
||||
: L_(luaL_newstate()), scriptDirectory_(scriptPath.parent_path()) {
|
||||
if (!L_) {
|
||||
throw std::runtime_error("Failed to create Lua state");
|
||||
}
|
||||
@@ -45,10 +47,30 @@ CubeScript::CubeScript(const std::filesystem::path& scriptPath) : L_(luaL_newsta
|
||||
L_ = nullptr;
|
||||
throw std::runtime_error("Failed to load Lua script: " + message);
|
||||
}
|
||||
|
||||
lua_getglobal(L_, "gui_input");
|
||||
if (!lua_isnil(L_, -1)) {
|
||||
guiInputRef_ = luaL_ref(L_, LUA_REGISTRYINDEX);
|
||||
} else {
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
|
||||
lua_getglobal(L_, "get_gui_commands");
|
||||
if (lua_isfunction(L_, -1)) {
|
||||
guiCommandsFnRef_ = luaL_ref(L_, LUA_REGISTRYINDEX);
|
||||
} else {
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
}
|
||||
|
||||
CubeScript::~CubeScript() {
|
||||
if (L_) {
|
||||
if (guiInputRef_ != LUA_REFNIL) {
|
||||
luaL_unref(L_, LUA_REGISTRYINDEX, guiInputRef_);
|
||||
}
|
||||
if (guiCommandsFnRef_ != LUA_REFNIL) {
|
||||
luaL_unref(L_, LUA_REGISTRYINDEX, guiCommandsFnRef_);
|
||||
}
|
||||
lua_close(L_);
|
||||
}
|
||||
}
|
||||
@@ -328,4 +350,205 @@ std::string CubeScript::LuaErrorMessage(lua_State* L) {
|
||||
return message ? message : "unknown lua error";
|
||||
}
|
||||
|
||||
std::vector<CubeScript::GuiCommand> CubeScript::LoadGuiCommands() {
|
||||
std::vector<GuiCommand> commands;
|
||||
if (guiCommandsFnRef_ == LUA_REFNIL) {
|
||||
return commands;
|
||||
}
|
||||
lua_rawgeti(L_, LUA_REGISTRYINDEX, guiCommandsFnRef_);
|
||||
if (lua_pcall(L_, 0, 1, 0) != LUA_OK) {
|
||||
std::string message = LuaErrorMessage(L_);
|
||||
lua_pop(L_, 1);
|
||||
throw std::runtime_error("Lua get_gui_commands failed: " + message);
|
||||
}
|
||||
if (!lua_istable(L_, -1)) {
|
||||
lua_pop(L_, 1);
|
||||
throw std::runtime_error("'get_gui_commands' did not return a table");
|
||||
}
|
||||
|
||||
size_t count = lua_rawlen(L_, -1);
|
||||
commands.reserve(count);
|
||||
|
||||
for (size_t i = 1; i <= count; ++i) {
|
||||
lua_rawgeti(L_, -1, static_cast<int>(i));
|
||||
if (!lua_istable(L_, -1)) {
|
||||
lua_pop(L_, 1);
|
||||
throw std::runtime_error("GUI command at index " + std::to_string(i) + " is not a table");
|
||||
}
|
||||
int commandIndex = lua_gettop(L_);
|
||||
lua_getfield(L_, commandIndex, "type");
|
||||
const char* typeName = lua_tostring(L_, -1);
|
||||
if (!typeName) {
|
||||
lua_pop(L_, 2);
|
||||
throw std::runtime_error("GUI command at index " + std::to_string(i) + " is missing a type");
|
||||
}
|
||||
GuiCommand command{};
|
||||
if (std::strcmp(typeName, "rect") == 0) {
|
||||
command.type = GuiCommand::Type::Rect;
|
||||
command.rect = ReadRect(L_, commandIndex);
|
||||
command.color = ReadColor(L_, commandIndex, GuiColor{0.0f, 0.0f, 0.0f, 1.0f});
|
||||
command.borderColor = ReadColor(L_, commandIndex, GuiColor{0.0f, 0.0f, 0.0f, 0.0f});
|
||||
lua_getfield(L_, commandIndex, "borderWidth");
|
||||
if (lua_isnumber(L_, -1)) {
|
||||
command.borderWidth = static_cast<float>(lua_tonumber(L_, -1));
|
||||
}
|
||||
lua_pop(L_, 1);
|
||||
} else if (std::strcmp(typeName, "text") == 0) {
|
||||
command.type = GuiCommand::Type::Text;
|
||||
ReadStringField(L_, commandIndex, "text", command.text);
|
||||
lua_getfield(L_, commandIndex, "fontSize");
|
||||
if (lua_isnumber(L_, -1)) {
|
||||
command.fontSize = static_cast<float>(lua_tonumber(L_, -1));
|
||||
}
|
||||
lua_pop(L_, 1);
|
||||
std::string align;
|
||||
if (ReadStringField(L_, commandIndex, "alignX", align)) {
|
||||
command.alignX = align;
|
||||
}
|
||||
if (ReadStringField(L_, commandIndex, "alignY", align)) {
|
||||
command.alignY = align;
|
||||
}
|
||||
lua_getfield(L_, commandIndex, "clipRect");
|
||||
if (lua_istable(L_, -1)) {
|
||||
command.clipRect = ReadRect(L_, -1);
|
||||
command.hasClipRect = true;
|
||||
}
|
||||
lua_pop(L_, 1);
|
||||
lua_getfield(L_, commandIndex, "bounds");
|
||||
if (lua_istable(L_, -1)) {
|
||||
command.bounds = ReadRect(L_, -1);
|
||||
command.hasBounds = true;
|
||||
}
|
||||
lua_pop(L_, 1);
|
||||
command.color = ReadColor(L_, commandIndex, GuiColor{1.0f, 1.0f, 1.0f, 1.0f});
|
||||
} else if (std::strcmp(typeName, "clip_push") == 0) {
|
||||
command.type = GuiCommand::Type::ClipPush;
|
||||
command.rect = ReadRect(L_, commandIndex);
|
||||
} else if (std::strcmp(typeName, "clip_pop") == 0) {
|
||||
command.type = GuiCommand::Type::ClipPop;
|
||||
} else if (std::strcmp(typeName, "svg") == 0) {
|
||||
command.type = GuiCommand::Type::Svg;
|
||||
ReadStringField(L_, commandIndex, "path", command.svgPath);
|
||||
command.rect = ReadRect(L_, commandIndex);
|
||||
command.svgTint = ReadColor(L_, commandIndex, GuiColor{1.0f, 1.0f, 1.0f, 0.0f});
|
||||
lua_getfield(L_, commandIndex, "tint");
|
||||
if (lua_istable(L_, -1)) {
|
||||
command.svgTint = ReadColor(L_, -1, command.svgTint);
|
||||
}
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
lua_pop(L_, 1); // pop type
|
||||
lua_pop(L_, 1); // pop command table
|
||||
commands.push_back(std::move(command));
|
||||
}
|
||||
|
||||
lua_pop(L_, 1);
|
||||
return commands;
|
||||
}
|
||||
|
||||
void CubeScript::UpdateGuiInput(const GuiInputSnapshot& input) {
|
||||
if (guiInputRef_ == LUA_REFNIL) {
|
||||
return;
|
||||
}
|
||||
lua_rawgeti(L_, LUA_REGISTRYINDEX, guiInputRef_);
|
||||
int stateIndex = lua_gettop(L_);
|
||||
|
||||
lua_getfield(L_, stateIndex, "resetTransient");
|
||||
lua_pushvalue(L_, stateIndex);
|
||||
lua_call(L_, 1, 0);
|
||||
|
||||
lua_getfield(L_, stateIndex, "setMouse");
|
||||
lua_pushvalue(L_, stateIndex);
|
||||
lua_pushnumber(L_, input.mouseX);
|
||||
lua_pushnumber(L_, input.mouseY);
|
||||
lua_pushboolean(L_, input.mouseDown);
|
||||
lua_call(L_, 4, 0);
|
||||
|
||||
lua_getfield(L_, stateIndex, "setWheel");
|
||||
lua_pushvalue(L_, stateIndex);
|
||||
lua_pushnumber(L_, input.wheel);
|
||||
lua_call(L_, 2, 0);
|
||||
|
||||
if (!input.textInput.empty()) {
|
||||
lua_getfield(L_, stateIndex, "addTextInput");
|
||||
lua_pushvalue(L_, stateIndex);
|
||||
lua_pushstring(L_, input.textInput.c_str());
|
||||
lua_call(L_, 2, 0);
|
||||
}
|
||||
|
||||
for (const auto& [key, pressed] : input.keyStates) {
|
||||
lua_getfield(L_, stateIndex, "setKey");
|
||||
lua_pushvalue(L_, stateIndex);
|
||||
lua_pushstring(L_, key.c_str());
|
||||
lua_pushboolean(L_, pressed);
|
||||
lua_call(L_, 3, 0);
|
||||
}
|
||||
|
||||
lua_pop(L_, 1);
|
||||
}
|
||||
|
||||
bool CubeScript::HasGuiCommands() const {
|
||||
return guiCommandsFnRef_ != LUA_REFNIL;
|
||||
}
|
||||
|
||||
std::filesystem::path CubeScript::GetScriptDirectory() const {
|
||||
return scriptDirectory_;
|
||||
}
|
||||
|
||||
GuiRect CubeScript::ReadRect(lua_State* L, int index) {
|
||||
GuiRect rect{};
|
||||
if (!lua_istable(L, index)) {
|
||||
return rect;
|
||||
}
|
||||
int absIndex = lua_absindex(L, index);
|
||||
auto readField = [&](const char* name, float defaultValue) -> float {
|
||||
lua_getfield(L, absIndex, name);
|
||||
float value = defaultValue;
|
||||
if (lua_isnumber(L, -1)) {
|
||||
value = static_cast<float>(lua_tonumber(L, -1));
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return value;
|
||||
};
|
||||
rect.x = readField("x", rect.x);
|
||||
rect.y = readField("y", rect.y);
|
||||
rect.width = readField("width", rect.width);
|
||||
rect.height = readField("height", rect.height);
|
||||
return rect;
|
||||
}
|
||||
|
||||
GuiColor CubeScript::ReadColor(lua_State* L, int index, const GuiColor& defaultColor) {
|
||||
GuiColor color = defaultColor;
|
||||
if (!lua_istable(L, index)) {
|
||||
return color;
|
||||
}
|
||||
int absIndex = lua_absindex(L, index);
|
||||
for (int component = 0; component < 4; ++component) {
|
||||
lua_rawgeti(L, absIndex, component + 1);
|
||||
if (lua_isnumber(L, -1)) {
|
||||
float value = static_cast<float>(lua_tonumber(L, -1));
|
||||
switch (component) {
|
||||
case 0: color.r = value; break;
|
||||
case 1: color.g = value; break;
|
||||
case 2: color.b = value; break;
|
||||
case 3: color.a = value; break;
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
bool CubeScript::ReadStringField(lua_State* L, int index, const char* name, std::string& outString) {
|
||||
int absIndex = lua_absindex(L, index);
|
||||
lua_getfield(L, absIndex, name);
|
||||
if (lua_isstring(L, -1)) {
|
||||
outString = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace sdl3cpp::script
|
||||
|
||||
@@ -13,6 +13,62 @@
|
||||
|
||||
namespace sdl3cpp::script {
|
||||
|
||||
struct GuiInputSnapshot {
|
||||
float mouseX = 0.0f;
|
||||
float mouseY = 0.0f;
|
||||
bool mouseDown = false;
|
||||
float wheel = 0.0f;
|
||||
std::string textInput;
|
||||
std::unordered_map<std::string, bool> keyStates;
|
||||
};
|
||||
|
||||
struct GuiRect {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
float width = 0;
|
||||
float height = 0;
|
||||
};
|
||||
|
||||
struct GuiColor {
|
||||
float r = 0;
|
||||
float g = 0;
|
||||
float b = 0;
|
||||
float a = 1.0f;
|
||||
};
|
||||
|
||||
struct GuiCommand {
|
||||
enum class Type {
|
||||
Rect,
|
||||
Text,
|
||||
ClipPush,
|
||||
ClipPop,
|
||||
Svg,
|
||||
};
|
||||
|
||||
struct RectData {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
float width = 0;
|
||||
float height = 0;
|
||||
};
|
||||
|
||||
Type type = Type::Rect;
|
||||
RectData rect;
|
||||
GuiColor color;
|
||||
GuiColor borderColor;
|
||||
float borderWidth = 0.0f;
|
||||
bool hasClipRect = false;
|
||||
RectData clipRect{};
|
||||
std::string text;
|
||||
float fontSize = 16.0f;
|
||||
std::string alignX = \"left\";
|
||||
std::string alignY = \"center\";
|
||||
std::string svgPath;
|
||||
GuiColor svgTint;
|
||||
RectData bounds{};
|
||||
bool hasBounds = false;
|
||||
};
|
||||
|
||||
class CubeScript {
|
||||
public:
|
||||
explicit CubeScript(const std::filesystem::path& scriptPath);
|
||||
@@ -34,6 +90,10 @@ public:
|
||||
std::array<float, 16> ComputeModelMatrix(int functionRef, float time);
|
||||
std::array<float, 16> GetViewProjectionMatrix(float aspect);
|
||||
std::unordered_map<std::string, ShaderPaths> LoadShaderPathsMap();
|
||||
std::vector<GuiCommand> LoadGuiCommands();
|
||||
void UpdateGuiInput(const GuiInputSnapshot& input);
|
||||
bool HasGuiCommands() const;
|
||||
std::filesystem::path GetScriptDirectory() const;
|
||||
|
||||
private:
|
||||
static std::array<float, 3> ReadVector3(lua_State* L, int index);
|
||||
@@ -42,8 +102,14 @@ private:
|
||||
static std::vector<uint16_t> ReadIndexArray(lua_State* L, int index);
|
||||
static std::string LuaErrorMessage(lua_State* L);
|
||||
static ShaderPaths ReadShaderPathsTable(lua_State* L, int index);
|
||||
static GuiRect ReadRect(lua_State* L, int index);
|
||||
static GuiColor ReadColor(lua_State* L, int index, const GuiColor& defaultColor);
|
||||
static bool ReadStringField(lua_State* L, int index, const char* name, std::string& outString);
|
||||
|
||||
lua_State* L_ = nullptr;
|
||||
int guiInputRef_ = LUA_REFNIL;
|
||||
int guiCommandsFnRef_ = LUA_REFNIL;
|
||||
std::filesystem::path scriptDirectory_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::script
|
||||
|
||||
152
third_party/font8x8_basic.h
vendored
Normal file
152
third_party/font8x8_basic.h
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 8x8 monochrome bitmap fonts for rendering
|
||||
* Author: Daniel Hepper <daniel@hepper.net>
|
||||
*
|
||||
* License: Public Domain
|
||||
*
|
||||
* Based on:
|
||||
* // Summary: font8x8.h
|
||||
* // 8x8 monochrome bitmap fonts for rendering
|
||||
* //
|
||||
* // Author:
|
||||
* // Marcel Sondaar
|
||||
* // International Business Machines (public domain VGA fonts)
|
||||
* //
|
||||
* // License:
|
||||
* // Public Domain
|
||||
*
|
||||
* Fetched from: http://dimensionalrift.homelinux.net/combuster/mos3/?p=viewsource&file=/modules/gfx/font8_8.asm
|
||||
**/
|
||||
|
||||
// Constant: font8x8_basic
|
||||
// Contains an 8x8 font map for unicode points U+0000 - U+007F (basic latin)
|
||||
char font8x8_basic[128][8] = {
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0000 (nul)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0001
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0002
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0003
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0004
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0005
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0006
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0007
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0008
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0009
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000A
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000B
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000C
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000D
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000E
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+000F
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0010
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0011
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0012
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0013
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0014
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0015
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0016
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0017
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0018
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0019
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001A
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001B
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001C
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001D
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001E
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+001F
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0020 (space)
|
||||
{ 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00}, // U+0021 (!)
|
||||
{ 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0022 (")
|
||||
{ 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00}, // U+0023 (#)
|
||||
{ 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00}, // U+0024 ($)
|
||||
{ 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00}, // U+0025 (%)
|
||||
{ 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00}, // U+0026 (&)
|
||||
{ 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0027 (')
|
||||
{ 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00}, // U+0028 (()
|
||||
{ 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00}, // U+0029 ())
|
||||
{ 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00}, // U+002A (*)
|
||||
{ 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00}, // U+002B (+)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+002C (,)
|
||||
{ 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00}, // U+002D (-)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+002E (.)
|
||||
{ 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // U+002F (/)
|
||||
{ 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00}, // U+0030 (0)
|
||||
{ 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00}, // U+0031 (1)
|
||||
{ 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00}, // U+0032 (2)
|
||||
{ 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00}, // U+0033 (3)
|
||||
{ 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00}, // U+0034 (4)
|
||||
{ 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00}, // U+0035 (5)
|
||||
{ 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00}, // U+0036 (6)
|
||||
{ 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00}, // U+0037 (7)
|
||||
{ 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00}, // U+0038 (8)
|
||||
{ 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00}, // U+0039 (9)
|
||||
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00}, // U+003A (:)
|
||||
{ 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x06}, // U+003B (;)
|
||||
{ 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00}, // U+003C (<)
|
||||
{ 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00}, // U+003D (=)
|
||||
{ 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00}, // U+003E (>)
|
||||
{ 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00}, // U+003F (?)
|
||||
{ 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00}, // U+0040 (@)
|
||||
{ 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A)
|
||||
{ 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B)
|
||||
{ 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C)
|
||||
{ 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D)
|
||||
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E)
|
||||
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F)
|
||||
{ 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G)
|
||||
{ 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H)
|
||||
{ 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0049 (I)
|
||||
{ 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00}, // U+004A (J)
|
||||
{ 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00}, // U+004B (K)
|
||||
{ 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00}, // U+004C (L)
|
||||
{ 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00}, // U+004D (M)
|
||||
{ 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00}, // U+004E (N)
|
||||
{ 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00}, // U+004F (O)
|
||||
{ 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00}, // U+0050 (P)
|
||||
{ 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00}, // U+0051 (Q)
|
||||
{ 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00}, // U+0052 (R)
|
||||
{ 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00}, // U+0053 (S)
|
||||
{ 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0054 (T)
|
||||
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00}, // U+0055 (U)
|
||||
{ 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0056 (V)
|
||||
{ 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00}, // U+0057 (W)
|
||||
{ 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00}, // U+0058 (X)
|
||||
{ 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00}, // U+0059 (Y)
|
||||
{ 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00}, // U+005A (Z)
|
||||
{ 0x1E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x1E, 0x00}, // U+005B ([)
|
||||
{ 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x40, 0x00}, // U+005C (\)
|
||||
{ 0x1E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x00}, // U+005D (])
|
||||
{ 0x08, 0x1C, 0x36, 0x63, 0x00, 0x00, 0x00, 0x00}, // U+005E (^)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF}, // U+005F (_)
|
||||
{ 0x0C, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+0060 (`)
|
||||
{ 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00}, // U+0061 (a)
|
||||
{ 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00}, // U+0062 (b)
|
||||
{ 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00}, // U+0063 (c)
|
||||
{ 0x38, 0x30, 0x30, 0x3e, 0x33, 0x33, 0x6E, 0x00}, // U+0064 (d)
|
||||
{ 0x00, 0x00, 0x1E, 0x33, 0x3f, 0x03, 0x1E, 0x00}, // U+0065 (e)
|
||||
{ 0x1C, 0x36, 0x06, 0x0f, 0x06, 0x06, 0x0F, 0x00}, // U+0066 (f)
|
||||
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0067 (g)
|
||||
{ 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00}, // U+0068 (h)
|
||||
{ 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+0069 (i)
|
||||
{ 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E}, // U+006A (j)
|
||||
{ 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00}, // U+006B (k)
|
||||
{ 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00}, // U+006C (l)
|
||||
{ 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00}, // U+006D (m)
|
||||
{ 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00}, // U+006E (n)
|
||||
{ 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00}, // U+006F (o)
|
||||
{ 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F}, // U+0070 (p)
|
||||
{ 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78}, // U+0071 (q)
|
||||
{ 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00}, // U+0072 (r)
|
||||
{ 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00}, // U+0073 (s)
|
||||
{ 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00}, // U+0074 (t)
|
||||
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00}, // U+0075 (u)
|
||||
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00}, // U+0076 (v)
|
||||
{ 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00}, // U+0077 (w)
|
||||
{ 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00}, // U+0078 (x)
|
||||
{ 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F}, // U+0079 (y)
|
||||
{ 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00}, // U+007A (z)
|
||||
{ 0x38, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x38, 0x00}, // U+007B ({)
|
||||
{ 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // U+007C (|)
|
||||
{ 0x07, 0x0C, 0x0C, 0x38, 0x0C, 0x0C, 0x07, 0x00}, // U+007D (})
|
||||
{ 0x6E, 0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // U+007E (~)
|
||||
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // U+007F
|
||||
};
|
||||
Reference in New Issue
Block a user