From 7fed9885413cdba9ee4706ab9a5c7f96f2b22422 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Mon, 9 Jan 2023 13:48:05 +0100 Subject: [PATCH 01/14] Virtual UI panels --- cmake/Root.cmake | 1 + include/linalg_shim.hpp | 2 + rust/src/shards/gui/mod.rs | 2 +- rust/src/shards/gui/raw_context.rs | 254 +++++++++++++++++++++++++++ src/core/exposed_type_utils.hpp | 5 + src/extra/CMakeLists.txt | 3 +- src/extra/egui/context.hpp | 20 +++ src/extra/runtime.cpp | 5 + src/extra/vui/vui.cpp | 257 ++++++++++++++++++++++++++++ src/extra/vui/vui.hpp | 37 ++++ src/gfx/checked_stack.hpp | 1 - src/gfx/egui/egui_render_pass.hpp | 86 ++++++---- src/gfx/egui/renderer.cpp | 88 +++++----- src/input/CMakeLists.txt | 6 +- src/input/input.cpp | 13 +- src/input/input.hpp | 4 +- src/input/tests/tests.cpp | 2 +- src/tests/vui.edn | 72 ++++++++ src/virtual_ui/CMakeLists.txt | 4 + src/virtual_ui/context.cpp | 264 +++++++++++++++++++++++++++++ src/virtual_ui/context.hpp | 64 +++++++ src/virtual_ui/panel.cpp | 4 + src/virtual_ui/panel.hpp | 39 +++++ 23 files changed, 1145 insertions(+), 88 deletions(-) create mode 100644 rust/src/shards/gui/raw_context.rs create mode 100644 src/extra/egui/context.hpp create mode 100644 src/extra/vui/vui.cpp create mode 100644 src/extra/vui/vui.hpp create mode 100644 src/tests/vui.edn create mode 100644 src/virtual_ui/CMakeLists.txt create mode 100644 src/virtual_ui/context.cpp create mode 100644 src/virtual_ui/context.hpp create mode 100644 src/virtual_ui/panel.cpp create mode 100644 src/virtual_ui/panel.hpp diff --git a/cmake/Root.cmake b/cmake/Root.cmake index e15ed0e187..bb5309d244 100644 --- a/cmake/Root.cmake +++ b/cmake/Root.cmake @@ -32,6 +32,7 @@ add_subdirectory(${SHARDS_DIR}/src/core src/core) add_subdirectory(${SHARDS_DIR}/src/mal src/mal) add_subdirectory(${SHARDS_DIR}/src/input src/input) add_subdirectory(${SHARDS_DIR}/src/gfx src/gfx) +add_subdirectory(${SHARDS_DIR}/src/virtual_ui src/virtual_ui) if(SHARDS_WITH_EXTRA_SHARDS) if(SHARDS_WITH_RUST_SHARDS) diff --git a/include/linalg_shim.hpp b/include/linalg_shim.hpp index 12991eeeb2..ecabfc4c82 100644 --- a/include/linalg_shim.hpp +++ b/include/linalg_shim.hpp @@ -6,6 +6,8 @@ #include #include +#include + namespace shards { struct alignas(16) Mat4 : public linalg::aliases::float4x4 { using linalg::aliases::float4x4::mat; diff --git a/rust/src/shards/gui/mod.rs b/rust/src/shards/gui/mod.rs index f9d3b90283..a3eb863308 100644 --- a/rust/src/shards/gui/mod.rs +++ b/rust/src/shards/gui/mod.rs @@ -112,7 +112,7 @@ mod misc; mod properties; mod util; mod widgets; - +mod raw_context; struct MutableVar<'a>(&'a mut Var); struct ImmutableVar<'a>(&'a Var); diff --git a/rust/src/shards/gui/raw_context.rs b/rust/src/shards/gui/raw_context.rs new file mode 100644 index 0000000000..65ca4e856b --- /dev/null +++ b/rust/src/shards/gui/raw_context.rs @@ -0,0 +1,254 @@ +use egui_gfx::egui_FullOutput; +use egui_gfx::egui_Input; +use egui_gfx::make_native_full_output; +use egui_gfx::NativeFullOutput; + +use super::util; +use super::EguiContext; +use super::CONTEXTS_NAME; +use super::EGUI_CTX_TYPE; +use super::EGUI_UI_SEQ_TYPE; +use super::GFX_CONTEXT_TYPE; +use super::GFX_QUEUE_VAR_TYPES; +use super::HELP_OUTPUT_EQUAL_INPUT; +use super::INPUT_CONTEXT_TYPE; +use super::PARENTS_UI_NAME; +use crate::core::Core; +use crate::shard::Shard; +use crate::shardsc; +use crate::shardsc::Shards; +use crate::types::Context; +use crate::types::ExposedInfo; +use crate::types::ExposedTypes; +use crate::types::InstanceData; +use crate::types::OptionalString; +use crate::types::ParamVar; +use crate::types::Parameters; +use crate::types::RawString; +use crate::types::Seq; +use crate::types::ShardsVar; +use crate::types::Type; +use crate::types::Var; +use crate::types::WireState; +use crate::types::ANY_TYPES; +use crate::types::SHARDS_OR_NONE_TYPES; +use std::ffi::CStr; + +struct UIContext { + context: Option, + full_output: Option, + instance: ParamVar, + parents: ParamVar, + exposed: Vec, +} + +impl UIContext { + pub fn default() -> Self { + let mut instance = ParamVar::default(); + instance.set_name(CONTEXTS_NAME); + + let mut parents = ParamVar::default(); + parents.set_name(PARENTS_UI_NAME); + + let exposed = vec![ + ExposedInfo { + exposedType: EGUI_CTX_TYPE, + name: instance.get_name(), + help: cstr!("The UI context.").into(), + isMutable: false, + isProtected: true, // don't allow to be used in code/wires + isTableEntry: false, + global: false, + isPushTable: false, + }, + ExposedInfo { + exposedType: EGUI_UI_SEQ_TYPE, + name: parents.get_name(), + help: cstr!("The parent UI objects.").into(), + isMutable: false, + isProtected: true, // don't allow to be used in code/wires + isTableEntry: false, + global: false, + isPushTable: false, + }, + ]; + + Self { + context: None, + full_output: None, + instance: instance, + parents: parents, + exposed: exposed, + } + } + + pub fn get_exposed_info(&self) -> &Vec { + &self.exposed + } + + pub fn warmup(&mut self, ctx: &Context) -> Result<(), &str> { + self.context = Some(egui::Context::default()); + self.instance.warmup(ctx); + self.parents.warmup(ctx); + + // Initialize the parents stack in the root UI. + // Every other UI elements will reference it and push or pop UIs to it. + if !self.parents.get().is_seq() { + self.parents.set(Seq::new().as_ref().into()); + } + + // Context works the same + if !self.instance.get().is_seq() { + self.instance.set(Seq::new().as_ref().into()); + } + + Ok(()) + } + + pub fn cleanup(&mut self) -> Result<(), &str> { + self.parents.cleanup(); + self.instance.cleanup(); + Ok(()) + } + + pub fn activate( + &mut self, + egui_input: &egui_Input, + contents: &Shards, + context: &Context, + input: &Var, + ) -> Result { + let gui_ctx = if let Some(gui_ctx) = &self.context { + gui_ctx + } else { + return Err("No UI context"); + }; + + let raw_input = egui_gfx::translate_raw_input(egui_input); + match raw_input { + Err(_error) => { + shlog_debug!("Input translation error: {:?}", _error); + Err("Input translation error") + } + Ok(raw_input) => { + let draw_scale = raw_input.pixels_per_point.unwrap_or(1.0); + + let mut error: Option<&str> = None; + let egui_output = gui_ctx.run(raw_input, |ctx| { + error = (|| -> Result<(), &str> { + // Push empty parent UI in case this context is nested inside another UI + util::update_seq(&mut self.parents, |seq| { + seq.push(Var::default()); + })?; + + let mut _output = Var::default(); + let wire_state: WireState = + util::with_object_stack_var(&mut self.instance, ctx, &EGUI_CTX_TYPE, || { + Ok(unsafe { + (*Core).runShards.unwrap()( + *contents, + context as *const _ as *mut _, + input, + &mut _output, + ) + .into() + }) + })?; + + if wire_state == WireState::Error { + return Err("Failed to activate UI contents"); + } + + // Pop empty parent UI + util::update_seq(&mut self.parents, |seq| { + seq.pop(); + })?; + + Ok(()) + })() + .err(); + }); + + if let Some(e) = error { + return Err(e); + } + + #[cfg(not(any(target_arch = "wasm32", target_os = "ios")))] + if let Some(url) = &egui_output.platform_output.open_url { + webbrowser::open(&url.url).map_err(|e| { + shlog_error!("{}", e); + "Failed to open URL." + })?; + } + + self.full_output = Some(make_native_full_output(&gui_ctx, egui_output, draw_scale)?); + Ok(*input) + } + } + } + + pub fn get_egui_output(&self) -> *const egui_FullOutput { + return &self.full_output.as_ref().unwrap().full_output; + } +} + +mod native { + use egui_gfx::{egui_FullOutput, egui_Input}; + + use super::UIContext; + use crate::{ + shardsc::{self, SHVar, Shards}, + types::{Context, Var}, + }; + + #[no_mangle] + unsafe extern "C" fn egui_createContext() -> *mut UIContext { + Box::into_raw(Box::new(UIContext::default())) + } + + #[no_mangle] + unsafe extern "C" fn egui_destroyContext(ptr: *mut UIContext) { + drop(Box::from_raw(ptr)) + } + + #[no_mangle] + unsafe extern "C" fn egui_getExposedTypeInfo( + ptr: *mut UIContext, + out_info: *mut shardsc::SHExposedTypesInfo, + ) { + (*out_info) = (&(*ptr).exposed).into(); + } + + #[no_mangle] + unsafe extern "C" fn egui_warmup(ptr: *mut UIContext, ctx: &Context) { + (*ptr).warmup(&ctx).unwrap(); + } + + #[no_mangle] + unsafe extern "C" fn egui_cleanup(ptr: *mut UIContext) { + (*ptr).cleanup().unwrap(); + } + + #[no_mangle] + unsafe extern "C" fn egui_activate( + ptr: *mut UIContext, + egui_input: *const egui_Input, + contents: &Shards, + context: &Context, + input: &SHVar, + output: *mut SHVar, + ) -> *const u8 { + match (*ptr).activate(&*egui_input, &contents, &context, &input) { + Ok(result) => { + *output = result; + std::ptr::null() + } + Err(str) => str.as_ptr(), + } + } + + #[no_mangle] + unsafe extern "C" fn egui_getOutput(ptr: *const UIContext) -> *const egui_FullOutput { + &(*ptr).full_output.as_ref().expect("Expected egui output").full_output + } +} diff --git a/src/core/exposed_type_utils.hpp b/src/core/exposed_type_utils.hpp index 3f8b027ff4..d0f8e9a7a7 100644 --- a/src/core/exposed_type_utils.hpp +++ b/src/core/exposed_type_utils.hpp @@ -97,6 +97,11 @@ inline void mergeIntoExposedInfo(ExposedInfo &outInfo, const ShardsVar &shardsVa outInfo.push_back(info); } +inline void mergeIntoExposedInfo(ExposedInfo &outInfo, const SHExposedTypesInfo &otherTypes) { + for (size_t i = 0; i < otherTypes.len; i++) + outInfo.push_back(otherTypes.elements[i]); +} + } // namespace shards #endif /* A16CC8A4_FBC4_4500_BE1D_F565963C9C16 */ diff --git a/src/extra/CMakeLists.txt b/src/extra/CMakeLists.txt index f4c4ee572c..b4015f3389 100644 --- a/src/extra/CMakeLists.txt +++ b/src/extra/CMakeLists.txt @@ -19,6 +19,7 @@ set(extra_SOURCES gfx/texture.cpp gfx/view.cpp gfx/steps.cpp + vui/vui.cpp gui.cpp rust_interop.cpp gizmos/context.cpp @@ -43,7 +44,7 @@ target_link_libraries(shards-extra SDL2) target_link_libraries(shards-extra shards-core - stb gfx gfx-gltf gfx-egui input + stb gfx gfx-gltf gfx-egui input virtual-ui brotlienc-static brotlidec-static brotlicommon-static snappy kissfft miniaudio nlohmann_json diff --git a/src/extra/egui/context.hpp b/src/extra/egui/context.hpp new file mode 100644 index 0000000000..3b7028edde --- /dev/null +++ b/src/extra/egui/context.hpp @@ -0,0 +1,20 @@ +#ifndef ED3C732D_AC03_4B15_8EC8_BB15262E26DA +#define ED3C732D_AC03_4B15_8EC8_BB15262E26DA + +#include +#include + +typedef void *EguiContext; + +extern "C" { +EguiContext egui_createContext(); +void egui_destroyContext(EguiContext); +void egui_getExposedTypeInfo(EguiContext, SHExposedTypesInfo &outTypes); +void egui_warmup(EguiContext, SHContext *shContext); +void egui_cleanup(EguiContext); +const char *egui_activate(EguiContext eguiContext, const egui::Input &eguiInput, const Shards &shards, SHContext *shContext, + const SHVar &input, SHVar &output); +const egui::FullOutput *egui_getOutput(const EguiContext eguiContext); +} + +#endif /* ED3C732D_AC03_4B15_8EC8_BB15262E26DA */ diff --git a/src/extra/runtime.cpp b/src/extra/runtime.cpp index e9d2f47b7d..efafd559e0 100644 --- a/src/extra/runtime.cpp +++ b/src/extra/runtime.cpp @@ -46,6 +46,10 @@ namespace Gizmos { extern void registerShards(); } +namespace VUI { +extern void registerShards(); +} + void shInitExtras() { #if SHARDS_WITH_RUST_SHARDS registerRustShards(shardsInterface(SHARDS_CURRENT_ABI)); @@ -60,6 +64,7 @@ void shInitExtras() { Audio::registerShards(); DSP::registerShards(); Gui::registerShards(); + VUI::registerShards(); #ifdef _WIN32 Desktop::registerDesktopShards(); diff --git a/src/extra/vui/vui.cpp b/src/extra/vui/vui.cpp new file mode 100644 index 0000000000..d2b9888747 --- /dev/null +++ b/src/extra/vui/vui.cpp @@ -0,0 +1,257 @@ +#include "vui.hpp" +#include "../gfx.hpp" +#include "../inputs.hpp" +#include "gfx/egui/egui_types.hpp" +#include "gfx/linalg.hpp" +#include "linalg_shim.hpp" +#include "object_var_util.hpp" +#include "params.hpp" +#include "../egui/context.hpp" +#include +#include +#include + +namespace shards::VUI { + +#define VUI_PANEL_SHARD_NAME "VUI.Panel" + +struct VUIPanelShard; +struct VUIContextShard { + VUIContext _vuiContext{}; + SHVar *_vuiContextVar{}; + + Inputs::RequiredInputContext _inputContext; + gfx::RequiredGraphicsContext _graphicsContext; + + ExposedInfo _exposedVariables; + + std::vector _panels; + + input::InputBuffer _inputBuffer; + + std::shared_ptr _debugRenderer; + + PARAM_PARAMVAR(_queue, "Queue", "The draw queue to insert draw commands into.", {Type::VariableOf(gfx::Types::DrawQueue)}); + PARAM_PARAMVAR(_view, "View", "The view that is being used to render.", {Type::VariableOf(gfx::Types::View)}); + PARAM(ShardsVar, _contents, "Contents", "The list of UI panels to render.", {CoreInfo::ShardsOrNone}); + PARAM_VAR(_scale, "Scale", "The scale of how many UI units per world unit.", {CoreInfo::FloatType}); + PARAM_VAR(_debug, "Debug", "Render debug.", {CoreInfo::BoolType}); + PARAM_IMPL(VUIContextShard, PARAM_IMPL_FOR(_queue), PARAM_IMPL_FOR(_view), PARAM_IMPL_FOR(_contents), PARAM_IMPL_FOR(_scale), + PARAM_IMPL_FOR(_debug)); + + VUIContextShard() { + _scale = Var(1000.0f); + _debug = Var(true); + } + + static SHTypesInfo inputTypes() { return CoreInfo::NoneType; } + static SHTypesInfo outputTypes() { return CoreInfo::NoneType; } + static SHOptionalString help() { + return SHCCSTR("Creates a context for virtual UI panels to make sure input is correctly handled between them"); + } + + SHExposedTypesInfo requiredVariables() { + static auto e = + exposedTypesOf(decltype(_inputContext)::getExposedTypeInfo(), decltype(_graphicsContext)::getExposedTypeInfo()); + return e; + } + + void warmup(SHContext *context); + + void cleanup() { + PARAM_CLEANUP() + + _contents.cleanup(); + _graphicsContext.cleanup(); + _inputContext.cleanup(); + + if (_vuiContextVar) { + if (_vuiContextVar->refcount > 1) { + SHLOG_ERROR("VUI.Context: Found {} dangling reference(s) to {}", _vuiContextVar->refcount - 1, VUIContext::VariableName); + } + releaseVariable(_vuiContextVar); + } + } + + SHTypeInfo compose(SHInstanceData &data) { + _exposedVariables = ExposedInfo(data.shared); + _exposedVariables.push_back(VUIContext::VariableInfo); + data.shared = SHExposedTypesInfo(_exposedVariables); + + _contents.compose(data); + + if (_queue->valueType == SHType::None) + throw ComposeError("Queue is required"); + + if (_view->valueType == SHType::None) + throw ComposeError("View is required"); + + return shards::CoreInfo::NoneType; + } + + SHVar activate(SHContext *shContext, const SHVar &input) { + auto &viewStack = _graphicsContext->renderer->getViewStack(); + auto viewStackTop = viewStack.getOutput(); + + auto &queue = *varAsObjectChecked(_queue.get(), gfx::Types::DrawQueue); + auto &view = *varAsObjectChecked(_view.get(), gfx::Types::View); + + // TODO: Move to input context + _inputBuffer.clear(); + for (auto &event : _inputContext->events) + _inputBuffer.push_back(event); + + // Evaluate all UI panels + _vuiContext.activationContext = shContext; + _vuiContext.context.virtualPointScale = _scale.payload.floatValue; + withObjectVariable(*_vuiContextVar, &_vuiContext, VUIContext::Type, [&]() { + gfx::float2 inputToViewScale{1.0f}; + gfx::SizedView sizedView(view.view, gfx::float2(viewStackTop.viewport.getSize())); + _vuiContext.context.prepareInputs(_inputBuffer, inputToViewScale, sizedView); + _vuiContext.context.evaluate(queue.queue, _graphicsContext->time, _graphicsContext->deltaTime); + }); + _vuiContext.activationContext = nullptr; + + // Render debug overlay + if ((bool)_debug) { + if (!_debugRenderer) { + _debugRenderer = std::make_shared(); + } + + _debugRenderer->begin(view.view, gfx::float2(viewStackTop.viewport.getSize())); + _vuiContext.context.renderDebug(_debugRenderer->getShapeRenderer()); + _debugRenderer->end(queue.queue); + } + + SHVar output{}; + return output; + } +}; + +struct VUIPanelShard { + PARAM_PARAMVAR(_transform, "Transform", "The world transform of this panel.", + {CoreInfo::Float4x4Type, Type::VariableOf(CoreInfo::Float4x4Type)}); + PARAM_PARAMVAR(_size, "Size", "The size of the panel.", {CoreInfo::Float2Type, Type::VariableOf(CoreInfo::Float2Type)}); + PARAM(ShardsVar, _contents, "Contents", "The panel UI contents.", {CoreInfo::ShardsOrNone}); + PARAM_IMPL(VUIPanelShard, PARAM_IMPL_FOR(_transform), PARAM_IMPL_FOR(_size), PARAM_IMPL_FOR(_contents)); + + RequiredVUIContext _context; + Shard *_uiShard{}; + EguiContext _eguiContext; + ExposedInfo _exposedTypes; + std::shared_ptr _panel; + + static SHTypesInfo inputTypes() { return CoreInfo::NoneType; } + static SHTypesInfo outputTypes() { return CoreInfo::NoneType; } + static SHOptionalString help() { return SHCCSTR("Defines a virtual UI panel"); } + + SHExposedTypesInfo requiredVariables() { + static auto e = exposedTypesOf(RequiredVUIContext::getExposedTypeInfo()); + return e; + } + + void ensureEguiContext() { + if (!_eguiContext) + _eguiContext = egui_createContext(); + } + + void warmup(SHContext *context) { + PARAM_WARMUP(context); + _context.warmup(context); + + ensureEguiContext(); + egui_warmup(_eguiContext, context); + + _panel = std::make_shared(*this); + _context->context.panels.emplace_back(_panel); + } + + void cleanup() { + PARAM_CLEANUP(); + _context.cleanup(); + + if (_eguiContext) { + egui_cleanup(_eguiContext); + egui_destroyContext(_eguiContext); + _eguiContext = nullptr; + } + + _panel.reset(); + } + + SHTypeInfo compose(SHInstanceData &data) { + ensureEguiContext(); + + SHExposedTypesInfo eguiExposedTypes{}; + egui_getExposedTypeInfo(_eguiContext, eguiExposedTypes); + + _exposedTypes = ExposedInfo(data.shared); + mergeIntoExposedInfo(_exposedTypes, eguiExposedTypes); + + // Compose contents + data.shared = SHExposedTypesInfo(_exposedTypes); + _contents.compose(data); + + return shards::CoreInfo::NoneType; + } + + SHVar activate(SHContext *shContext, const SHVar &input) { + throw ActivationError("Invalid activation, VUIPanel can not be used directly"); + } + + // This evaluates the egui contents for this panel + virtual const egui::FullOutput &render(const egui::Input &inputs) { + SHVar output{}; + const char *error = egui_activate(_eguiContext, inputs, _contents.shards(), _context->activationContext, SHVar{}, output); + if (error) + throw ActivationError(fmt::format("egui activation error: {}", error)); + + const egui::FullOutput &eguiOutput = *egui_getOutput(_eguiContext); + return eguiOutput; + } + + vui::PanelGeometry getGeometry() const { + gfx::float4x4 transform = toFloat4x4(_transform.get()); + gfx::float2 alignment{0.5f}; + + vui::PanelGeometry result; + result.anchor = gfx::extractTranslation(transform); + result.up = transform.y.xyz(); + result.right = transform.x.xyz(); + result.size = toFloat2(_size.get()); + result.center = + result.anchor + result.right * (0.5f - alignment.x) * result.size.x + result.up * (0.5f - alignment.y) * result.size.y; + return result; + } +}; + +const egui::FullOutput &Panel::render(const egui::Input &inputs) { return panelShard.render(inputs); } +vui::PanelGeometry Panel::getGeometry() const { return panelShard.getGeometry(); } + +void VUIContextShard::warmup(SHContext *context) { + _vuiContextVar = referenceVariable(context, VUIContext::VariableName); + + _inputContext.warmup(context); + _graphicsContext.warmup(context); + + withObjectVariable(*_vuiContextVar, &_vuiContext, VUIContext::Type, [&]() { PARAM_WARMUP(context); }); + + // Collect VUI.Panel shards similar to UI dock does + // _panels.clear(); + // auto contentShards = _contents.shards(); + // for (size_t i = 0; i < contentShards.len; i++) { + // ShardPtr shard = contentShards.elements[i]; + + // if (std::string(VUI_PANEL_SHARD_NAME) == shard->name(shard)) { + // using ShardWrapper = shards::ShardWrapper; + // VUIPanelShard &vuiPanel = reinterpret_cast(shard)->shard; + // _panels.emplace_back(&vuiPanel); + // } + // } +} + +void registerShards() { + REGISTER_SHARD("VUI.Context", VUIContextShard); + REGISTER_SHARD(VUI_PANEL_SHARD_NAME, VUIPanelShard); +} +} // namespace shards::VUI diff --git a/src/extra/vui/vui.hpp b/src/extra/vui/vui.hpp new file mode 100644 index 0000000000..788c95b9fd --- /dev/null +++ b/src/extra/vui/vui.hpp @@ -0,0 +1,37 @@ +#ifndef A70CCFB2_9913_4743_9046_5624A7B1ED9A +#define A70CCFB2_9913_4743_9046_5624A7B1ED9A + +#include +#include +#include +#include +#include +#include + +namespace shards::VUI { + +struct Panel : public shards::vui::Panel { + struct VUIPanelShard &panelShard; + + Panel(VUIPanelShard &panelShard) : panelShard(panelShard) {} + virtual const egui::FullOutput &render(const egui::Input &inputs); + virtual vui::PanelGeometry getGeometry() const; +}; + +struct VUIContext { + static constexpr uint32_t TypeId = 'vuic'; + static inline SHTypeInfo Type{SHType::Object, {.object = {.vendorId = CoreCC, .typeId = TypeId}}}; + static inline const char VariableName[] = "VUI.Context"; + static inline const SHOptionalString VariableDescription = SHCCSTR("The virtual UI context."); + static inline SHExposedTypeInfo VariableInfo = shards::ExposedInfo::ProtectedVariable(VariableName, VariableDescription, Type); + + SHContext *activationContext{}; + vui::Context context; + std::vector panels; +}; + +typedef shards::RequiredContextVariable RequiredVUIContext; + +} // namespace shards::VUI + +#endif /* A70CCFB2_9913_4743_9046_5624A7B1ED9A */ diff --git a/src/gfx/checked_stack.hpp b/src/gfx/checked_stack.hpp index bd1273fa3a..1e33e52c24 100644 --- a/src/gfx/checked_stack.hpp +++ b/src/gfx/checked_stack.hpp @@ -14,7 +14,6 @@ template inline void ensureEmptyStack(std::vector &stack) { throw std::runtime_error(fmt::format("Stack was not empty({} elements not popped)", numItems)); } } - } // namespace gfx #endif /* A67E2D4D_B267_4EFB_9A3C_C2BF0D2FE420 */ diff --git a/src/gfx/egui/egui_render_pass.hpp b/src/gfx/egui/egui_render_pass.hpp index cb406f1b29..d774c1e930 100644 --- a/src/gfx/egui/egui_render_pass.hpp +++ b/src/gfx/egui/egui_render_pass.hpp @@ -7,26 +7,33 @@ #include "../features/transform.hpp" namespace gfx { -struct EguiRenderPass { - static PipelineStepPtr createPipelineStep(DrawQueuePtr queue) { - auto drawableStep = makePipelineStep(RenderDrawablesStep{ - .drawQueue = queue, - .sortMode = SortMode::Queue, - .features = - std::vector{ - createFeature(true), - }, - }); - return drawableStep; +struct EguiTransformFeature { + static FeaturePtr create() { + using namespace shader::blocks; + auto feature = std::make_shared(); + auto code = makeCompoundBlock(); + + code->appendLine("var vp = ", ReadBuffer("viewport", shader::FieldTypes::Float4, "view")); + code->appendLine("var p1 = ", ReadInput("position"), " * (1.0 / vp.zw)"); + code->appendLine("p1.y = -p1.y;"); + code->appendLine("p1 = p1 * 2.0 + vec2(-1.0, 1.0)"); + code->appendLine("var p4 = vec4(p1.xy, 0.0, 1.0)"); + code->appendLine(WriteOutput("position", shader::FieldTypes::Float4, "p4")); + + feature->shaderEntryPoints.emplace_back("uiBaseTransform", ProgrammableGraphicsStage::Vertex, std::move(code)); + + return feature; } +}; - static FeaturePtr createFeature(bool writeUiPosition) { +struct EguiColorFeature { + static FeaturePtr create() { using namespace shader::blocks; - auto uiFeature = std::make_shared(); + auto feature = std::make_shared(); auto code = makeCompoundBlock(); code->append(Header(R"( -fn linear_from_srgb(srgb: vec3) -> vec3 { +fn egui_linear_from_srgb(srgb: vec3) -> vec3 { let srgb_bytes = srgb * 255.0; let cutoff = srgb_bytes < vec3(10.31475); let lower = srgb_bytes / vec3(3294.6); @@ -34,19 +41,13 @@ fn linear_from_srgb(srgb: vec3) -> vec3 { return select(higher, lower, cutoff); })")); - if (writeUiPosition) { - code->appendLine("var vp = ", ReadBuffer("viewport", shader::FieldTypes::Float4, "view")); - code->appendLine("var p1 = ", ReadInput("position"), " * (1.0 / vp.zw)"); - code->appendLine("p1.y = -p1.y;"); - code->appendLine("p1 = p1 * 2.0 + vec2(-1.0, 1.0)"); - code->appendLine("var p4 = vec4(p1.xy, 0.0, 1.0)"); - code->appendLine(WriteOutput("position", shader::FieldTypes::Float4, "p4")); - } - code->appendLine("var color = ", ReadInput("color")); - code->appendLine(WriteOutput("color", shader::FieldTypes::Float4, "vec4(linear_from_srgb(color.xyz), color.a)")); + code->appendLine(WriteOutput("color", shader::FieldTypes::Float4, "vec4(egui_linear_from_srgb(color.xyz), color.a)")); - uiFeature->shaderEntryPoints.emplace_back("baseTransform", ProgrammableGraphicsStage::Vertex, std::move(code)); + auto &writeVertColor = + feature->shaderEntryPoints.emplace_back("writeUiColor", ProgrammableGraphicsStage::Vertex, std::move(code)); + // NOTE: Override BaseColor's writeColor + writeVertColor.dependencies.emplace_back("writeColor"); shader::FieldType flagsFieldType = shader::FieldType(ShaderFieldBaseType::UInt32, 1); code = makeCompoundBlock(); @@ -55,19 +56,38 @@ fn linear_from_srgb(srgb: vec3) -> vec3 { code->appendLine("var isFont = ", ReadBuffer("flags", flagsFieldType), " & 1u"); code->append("if(isFont != 0u) { texColor = vec4(texColor.xxx, texColor.x); }\n"); code->appendLine(WriteOutput("color", shader::FieldTypes::Float4, "color * texColor")); - uiFeature->shaderEntryPoints.emplace_back("color", ProgrammableGraphicsStage::Fragment, std::move(code)); - uiFeature->textureParams.emplace_back("color"); - uiFeature->shaderParams.emplace_back("flags", flagsFieldType, uint32_t(0)); + auto &writeFragColor = + feature->shaderEntryPoints.emplace_back("writeUiColor", ProgrammableGraphicsStage::Fragment, std::move(code)); + // NOTE: Override BaseColor's writeColor + writeFragColor.dependencies.emplace_back("writeColor"); + + feature->textureParams.emplace_back("color"); + feature->shaderParams.emplace_back("flags", flagsFieldType, uint32_t(0)); - uiFeature->state.set_depthWrite(false); - uiFeature->state.set_depthCompare(WGPUCompareFunction_Always); - uiFeature->state.set_culling(false); - uiFeature->state.set_blend(BlendState{ + feature->state.set_blend(BlendState{ .color = BlendComponent::AlphaPremultiplied, .alpha = BlendComponent::Opaque, }); - return uiFeature; + feature->state.set_depthWrite(false); + feature->state.set_depthCompare(WGPUCompareFunction_Always); + feature->state.set_culling(false); + return feature; + } +}; + +struct EguiRenderPass { + static PipelineStepPtr createPipelineStep(DrawQueuePtr queue) { + auto drawableStep = makePipelineStep(RenderDrawablesStep{ + .drawQueue = queue, + .sortMode = SortMode::Queue, + .features = + std::vector{ + EguiTransformFeature::create(), + EguiColorFeature::create(), + }, + }); + return drawableStep; } }; } // namespace gfx diff --git a/src/gfx/egui/renderer.cpp b/src/gfx/egui/renderer.cpp index 530390ba7f..ced359dc5e 100644 --- a/src/gfx/egui/renderer.cpp +++ b/src/gfx/egui/renderer.cpp @@ -145,10 +145,12 @@ struct EguiRendererImpl { TextureManager textures; std::vector pendingTextureFrees; std::vector drawables; + std::vector uiFeatures; MeshFormat meshFormat; EguiRendererImpl() { + uiFeatures.push_back(EguiColorFeature::create()); meshFormat = { .primitiveType = PrimitiveType::TriangleList, .windingOrder = WindingOrder::CCW, @@ -166,54 +168,58 @@ struct EguiRendererImpl { } pendingTextureFrees.clear(); } -}; -EguiRenderer::EguiRenderer() { impl = std::make_shared(); } + void render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue) { + meshPool.reset(); + processPendingTextureFrees(); -void EguiRenderer::render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue) { - impl->meshPool.reset(); - impl->processPendingTextureFrees(); - - // Update textures before render - auto &textureUpdates = output.textureUpdates; - for (size_t i = 0; i < textureUpdates.numSets; i++) { - auto &textureSet = textureUpdates.sets[i]; - SPDLOG_LOGGER_INFO(logger, "textureSet {}", (uint64_t)textureSet.id); - impl->textures.set(textureSet); - } + // Update textures before render + auto &textureUpdates = output.textureUpdates; + for (size_t i = 0; i < textureUpdates.numSets; i++) { + auto &textureSet = textureUpdates.sets[i]; + SPDLOG_LOGGER_INFO(logger, "textureSet {}", (uint64_t)textureSet.id); + textures.set(textureSet); + } - // Update meshes & generate drawables - auto &drawables = impl->drawables; - impl->drawables.resize(output.numPrimitives); - for (size_t i = 0; i < output.numPrimitives; i++) { - auto &prim = output.primitives[i]; - - MeshPtr mesh = impl->meshPool.allocateBuffer(prim.numVertices); - mesh->update(impl->meshFormat, prim.vertices, prim.numVertices * sizeof(egui::Vertex), prim.indices, - prim.numIndices * sizeof(uint32_t)); - - MeshDrawable &drawable = drawables[i]; - drawable.mesh = mesh; - drawable.transform = rootTransform; - TexturePtr texture = impl->textures.get(prim.textureId); - if (texture) { - drawable.parameters.set("color", texture); - if (texture->getFormat().pixelFormat == WGPUTextureFormat_R8Unorm) { - drawable.parameters.set("flags", uint32_t(0x1)); + // Update meshes & generate drawables + drawables.resize(output.numPrimitives); + for (size_t i = 0; i < output.numPrimitives; i++) { + auto &prim = output.primitives[i]; + + MeshPtr mesh = meshPool.allocateBuffer(prim.numVertices); + mesh->update(meshFormat, prim.vertices, prim.numVertices * sizeof(egui::Vertex), prim.indices, + prim.numIndices * sizeof(uint32_t)); + + MeshDrawable &drawable = drawables[i]; + drawable.mesh = mesh; + drawable.transform = rootTransform; + drawable.features = uiFeatures; + TexturePtr texture = textures.get(prim.textureId); + if (texture) { + drawable.parameters.set("color", texture); + if (texture->getFormat().pixelFormat == WGPUTextureFormat_R8Unorm) { + drawable.parameters.set("flags", uint32_t(0x1)); + } } - } - drawable.clipRect = int4(prim.clipRect.min.x, prim.clipRect.min.y, prim.clipRect.max.x, prim.clipRect.max.y); + // drawable.clipRect = int4(prim.clipRect.min.x, prim.clipRect.min.y, prim.clipRect.max.x, prim.clipRect.max.y); - drawQueue->add(drawable); - } + drawQueue->add(drawable); + } - // Store texture id's to free - // these are executed at the start the next time render() is called - for (size_t i = 0; i < textureUpdates.numFrees; i++) { - auto &id = textureUpdates.frees[i]; - impl->addPendingTextureFree(id); + // Store texture id's to free + // these are executed at the start the next time render() is called + for (size_t i = 0; i < textureUpdates.numFrees; i++) { + auto &id = textureUpdates.frees[i]; + addPendingTextureFree(id); + } } +}; + +EguiRenderer::EguiRenderer() { impl = std::make_shared(); } + +void EguiRenderer::render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue) { + impl->render(output, rootTransform, drawQueue); } void EguiRenderer::renderNoTransform(const egui::FullOutput &output, const gfx::DrawQueuePtr &drawQueue) { @@ -221,7 +227,9 @@ void EguiRenderer::renderNoTransform(const egui::FullOutput &output, const gfx:: } EguiRenderer *EguiRenderer::create() { return new EguiRenderer(); } + void EguiRenderer::destroy(EguiRenderer *renderer) { delete renderer; } + float EguiRenderer::getDrawScale(Window &window) { float2 drawScaleVec = window.getUIScale(); return std::max(drawScaleVec.x, drawScaleVec.y); diff --git a/src/input/CMakeLists.txt b/src/input/CMakeLists.txt index b29e820ad9..ef9e47104a 100644 --- a/src/input/CMakeLists.txt +++ b/src/input/CMakeLists.txt @@ -4,6 +4,6 @@ target_link_libraries(input SDL2 linalg spdlog) target_link_libraries(input gfx) # For window mapping target_compile_features(input PUBLIC cxx_std_20) -add_executable(test_input tests/tests.cpp) -target_link_libraries(test_input input Catch2Main Catch2 linalg spdlog) -target_compile_features(test_input PUBLIC cxx_std_20) +add_executable(test-input tests/tests.cpp) +target_link_libraries(test-input input Catch2Main Catch2 linalg spdlog) +target_compile_features(test-input PUBLIC cxx_std_20) diff --git a/src/input/input.cpp b/src/input/input.cpp index 07f6226c42..e96bcf4566 100644 --- a/src/input/input.cpp +++ b/src/input/input.cpp @@ -21,18 +21,18 @@ ConsumeEventFilter categorizeEvent(const SDL_Event &event) { case SDL_CONTROLLERTOUCHPADUP: case SDL_CONTROLLERSENSORUPDATE: return ConsumeEventFilter::Controller; - case SDL_MOUSEMOTION: - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEWHEEL: case SDL_DOLLARGESTURE: case SDL_DOLLARRECORD: case SDL_MULTIGESTURE: case SDL_FINGERDOWN: case SDL_FINGERMOTION: - return ConsumeEventFilter::PointerDown; case SDL_FINGERUP: + return ConsumeEventFilter::Touch; + case SDL_MOUSEWHEEL: case SDL_MOUSEBUTTONUP: - return ConsumeEventFilter::PointerUp; + case SDL_MOUSEMOTION: + case SDL_MOUSEBUTTONDOWN: + return ConsumeEventFilter::Mouse; case SDL_KEYDOWN: case SDL_KEYUP: case SDL_TEXTEDITING: @@ -44,6 +44,7 @@ ConsumeEventFilter categorizeEvent(const SDL_Event &event) { return ConsumeEventFilter::None; } } + void InputBuffer::consumeEvents(ConsumeEventFilter filter, void *by) { for (auto it = begin(); it; ++it) { ConsumeEventFilter type = categorizeEvent(*it); @@ -51,4 +52,4 @@ void InputBuffer::consumeEvents(ConsumeEventFilter filter, void *by) { it.consume(by); } } -} // namespace shards::input \ No newline at end of file +} // namespace shards::input diff --git a/src/input/input.hpp b/src/input/input.hpp index 01d19a520b..1ade8aeea4 100644 --- a/src/input/input.hpp +++ b/src/input/input.hpp @@ -10,8 +10,8 @@ namespace shards::input { enum class ConsumeEventFilter : uint8_t { None = 0, Keyboard = 1 << 0, - PointerDown = 1 << 1, - PointerUp = 1 << 2, + Mouse = 1 << 1, + Touch = 1 << 2, Controller = 1 << 3, }; diff --git a/src/input/tests/tests.cpp b/src/input/tests/tests.cpp index bb55b4b3ef..df60840418 100644 --- a/src/input/tests/tests.cpp +++ b/src/input/tests/tests.cpp @@ -178,7 +178,7 @@ TEST_CASE("Consume all mouse events") { generateTestInputMotionUpDown(buffer, 0, 0, 0); CHECK(buffer.size() > 0); - buffer.consumeEvents(ConsumeEventFilter::PointerDown | ConsumeEventFilter::PointerUp); + buffer.consumeEvents(ConsumeEventFilter::Touch | ConsumeEventFilter::Mouse); consumer.handleInput(buffer); CHECK(consumer.receivedEventCount == 0); diff --git a/src/tests/vui.edn b/src/tests/vui.edn new file mode 100644 index 0000000000..e99e2682b9 --- /dev/null +++ b/src/tests/vui.edn @@ -0,0 +1,72 @@ +(def timestep (/ 1.0 120.0)) +(defmesh Root) +(defloop test-wire + (Setup + 0.0 >= .time + + (GFX.DrawQueue) >= .queue + + (GFX.BuiltinMesh :Type BuiltinMeshType.Cube) >= .mesh + (Float3 0 0 0) (Math.Translation) >= .transform-0 + {:Mesh .mesh :Params {:baseColor (Float4 1 0 1 1)}} (GFX.Drawable :Transform .transform-0) >> .drawables + + ; Create render steps + (GFX.BuiltinFeature :Id BuiltinFeatureId.Transform) >> .features + (GFX.BuiltinFeature :Id BuiltinFeatureId.BaseColor) >> .features + {:Features .features :Queue .queue} (GFX.DrawablePass) >> .render-steps + + ; Initial panel transforms + 15.0 (Math.DegreesToRadians) (Math.AxisAngleY) (Math.Rotation) >= .tmp + (Float3 -1.0 0.0 0.0) (Math.Translation) (Math.MatMul .tmp) >= .panel-t-0 + -15.0 (Math.DegreesToRadians) (Math.AxisAngleY) (Math.Rotation) >= .tmp + (Float3 1.0 0.0 0.0) (Math.Translation) (Math.MatMul .tmp) >= .panel-t-1 + 5.0 (Math.DegreesToRadians) (Math.AxisAngleX) (Math.Rotation) >= .tmp + (Float3 0.0 1.2 0.0) (Math.Translation) (Math.MatMul .tmp) >= .panel-t-2 + + ; Initial view + {:Position (Float3 1 2 10) :Target (Float3 0 0 0)} (Math.LookAt) >= .view-transform + (GFX.View :View .view-transform) >= .view) + + (GFX.MainWindow + :Title "SDL Window" :Width 1280 :Height 720 :Debug false + :Contents + (-> + + ; Update view transform + .view-transform (FreeCamera :FlySpeed 10.0) > .view-transform + + .queue (GFX.ClearQueue) + (VUI.Context :Queue .queue :View .view :Scale 100.0 :Contents + (-> + (VUI.Panel :Transform .panel-t-0 :Size (Float2 100 100) :Contents + (-> + (UI.CentralPanel (-> + "First panel" (UI.Label) + (UI.Button :Label "Button") + ; + )))) + (VUI.Panel :Transform .panel-t-1 :Size (Float2 100 100) :Contents + (-> + (UI.CentralPanel (-> + "Some other panel" (UI.Label) + (UI.Button :Label "Button") + ; + )))) + (VUI.Panel :Transform .panel-t-2 :Size (Float2 300 60) :Contents + (-> + (UI.CentralPanel (-> + "Wide panel" (UI.Label) + (Setup + (LoadImage "../../assets/ShardsLogo.png") (GFX.Texture) >= .button-texture) + .button-texture (UI.ImageButton :Scale (Float2 0.1)) + ; + )))) + ; + )) + + .drawables (GFX.Draw .queue) + + (GFX.Render :Steps .render-steps :View .view)))) + +(schedule Root test-wire) +(if (run Root timestep) nil (throw "Root tick failed")) diff --git a/src/virtual_ui/CMakeLists.txt b/src/virtual_ui/CMakeLists.txt new file mode 100644 index 0000000000..ba84b6ea89 --- /dev/null +++ b/src/virtual_ui/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(virtual-ui context.cpp panel.cpp) +target_include_directories(virtual-ui PUBLIC ${CMAKE_CURRENT_LIST_DIR}/..) +target_link_libraries(virtual-ui SDL2 gfx) +target_compile_features(virtual-ui PUBLIC cxx_std_20) diff --git a/src/virtual_ui/context.cpp b/src/virtual_ui/context.cpp new file mode 100644 index 0000000000..d2a5f6cf50 --- /dev/null +++ b/src/virtual_ui/context.cpp @@ -0,0 +1,264 @@ +#include "context.hpp" +#include "gfx/egui/egui_types.hpp" +#include +#include +#include +#include +#include +#include + +using namespace gfx; +using shards::input::ConsumeEventFilter; +namespace shards::vui { + +void Context::prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewScale, const gfx::SizedView &sizedView) { + pointerInputs.clear(); + otherEvents.clear(); + for (auto it = input.begin(); it; ++it) { + bool isPointerEvent = false; + float2 pointerCoords; + switch (it->type) { + case SDL_MOUSEMOTION: + pointerCoords = float2(it->motion.x, it->motion.y); + isPointerEvent = true; + break; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + pointerCoords = float2(it->button.x, it->button.y); + isPointerEvent = true; + break; + } + + if (isPointerEvent) { + PointerInput outEvt; + outEvt.iterator = it; + outEvt.ray = sizedView.getRay(pointerCoords * inputToViewScale); + pointerInputs.push_back(outEvt); + } else { + otherEvents.push_back(it); + } + } + + struct PanelInput { + std::vector inputEvents; + }; + + std::vector panelEvents; + + float uiToWorldScale = 1.0f / virtualPointScale; + + lastFocusedPanel = focusedPanel; + for (auto &evt : pointerInputs) { + for (size_t panelIndex = 0; panelIndex < panels.size(); panelIndex++) { + auto &panel = panels[panelIndex]; + + auto geom = panel->getGeometry().scaled(uiToWorldScale); + + float3 planePoint = geom.center; + float3 planeNormal = linalg::cross(geom.right, geom.up); + + float dist{}; + if (gfx::intersectPlane(evt.ray.origin, evt.ray.direction, planePoint, planeNormal, dist) && dist < evt.hitDistance) { + + float3 hitPoint = evt.ray.origin + evt.ray.direction * dist; + + float3 topLeft = geom.getTopLeft(); + float fX = linalg::dot(hitPoint, geom.right) - linalg::dot(topLeft, geom.right); + float fY = -(linalg::dot(hitPoint, geom.up) - linalg::dot(topLeft, geom.up)); + if (fX >= 0.0f && fX <= geom.size.x) { + if (fY >= 0.0f && fY <= geom.size.y) { + evt.hitDistance = dist; + evt.panelCoord = float2(fX, fY); + focusedPanel = evt.hitPanel = panel; + } + } + } + } + } + + // Update last pointer input when new input events are received + if (pointerInputs.size() > 0) { + lastPointerInput.reset(); + for (auto &evt : pointerInputs) { + if (evt.hitPanel) { + if (!lastPointerInput || lastPointerInput->hitDistance > evt.hitDistance) { + lastPointerInput = evt; + } + } + } + } +} + +void Context::renderDebug(gfx::ShapeRenderer &sr) { + float uiToWorldScale = 1.0f / virtualPointScale; + + for (const auto &panel : panels) { + auto geom = panel->getGeometry().scaled(uiToWorldScale); + float4 c = panel == focusedPanel ? float4(0, 1, 0, 1) : float4(0.8, 0.8, 0.8, 1.0); + sr.addRect(geom.center, geom.right, geom.up, geom.size, c, 2); + // panel->transform + } + + bool pointerEventVisualized = false; + auto visualizePointerInput = [&](const PointerInput &pointerInput, const float4 &color = float4(1.0f, 1.0f, 1.0f, 1.0f)) { + if (pointerInput.hitPanel) { + auto geom = pointerInput.hitPanel->getGeometry().scaled(uiToWorldScale); + float3 hitCoord = geom.getTopLeft() + pointerInput.panelCoord.x * geom.right - pointerInput.panelCoord.y * geom.up; + sr.addPoint(hitCoord, color, 5); + pointerEventVisualized = true; + } + }; + + // Visulize new events + for (const auto &pointerInput : pointerInputs) { + visualizePointerInput(pointerInput); + } + + // Vizualize last event if no new input events were generatedz + if (lastPointerInput && !pointerEventVisualized) { + visualizePointerInput(lastPointerInput.value(), float4(0.5f, 0.5f, 0.5f, 1.0f)); + } +} + +struct ContextCachedPanel { + gfx::EguiRenderer renderer; +}; + +struct RenderContext { + PanelPtr panel; + gfx::DrawQueuePtr queue; + std::shared_ptr cached; + float scaling; + float pixelsPerPoint; +}; + +static void renderPanel(const RenderContext &ctx, const egui::FullOutput &output) { + EguiRenderer &renderer = ctx.cached->renderer; + PanelPtr panel = ctx.panel; + + float uiToWorldScale = 1.0f / ctx.scaling; + + float4x4 rootTransform = linalg::identity; + PanelGeometry geom = panel->getGeometry().scaled(uiToWorldScale); + + // Geometry comes in in UI coordinates, and it's also scaled by pixelsPerPoint passed to egui + // so it needs to be scaled by `1.0f / (scale * pixelsPerPoint)` + rootTransform = linalg::mul(linalg::scaling_matrix(float3(1.0f / (ctx.scaling * ctx.pixelsPerPoint))), rootTransform); + + // Derive rotation matrix from panel right/up + float4x4 rotationMat; + rotationMat.x = float4(geom.right, 0); + rotationMat.y = float4(-geom.up, 0); // Render UI as using Y+ is down + rotationMat.z = float4(linalg::cross(geom.right, geom.up), 0); + rotationMat.w = float4(0, 0, 0, 1); + rootTransform = linalg::mul(rotationMat, rootTransform); + + // Place drawable at top-left corner of panel UI + rootTransform = linalg::mul(linalg::translation_matrix(geom.getTopLeft()), rootTransform); + + renderer.render(output, rootTransform, ctx.queue); +} + +void Context::evaluate(gfx::DrawQueuePtr queue, double time, float deltaTime) { + bool panelFocusChanged = lastFocusedPanel != focusedPanel; + + for (auto &panel : panels) { + eguiInputTranslator.begin(time, deltaTime); + + if (panelFocusChanged && lastFocusedPanel == panel) { + egui::InputEvent evt; + auto &pointerGone = evt.pointerGone; + pointerGone.type = egui::InputEventType::PointerGone; + eguiInputTranslator.pushEvent(evt); + + // Simulate deselect + auto &pointerButton = evt.pointerButton; + pointerButton.type = egui::InputEventType::PointerButton; + pointerButton.button = egui::PointerButton::Primary; + pointerButton.pos = egui::toPos2(float2(-1, -1)); + pointerButton.type = egui::InputEventType::PointerButton; + pointerButton.pressed = true; + eguiInputTranslator.pushEvent(evt); + pointerButton.pressed = false; + eguiInputTranslator.pushEvent(evt); + } + + for (auto &pointerInput : pointerInputs) { + if (pointerInput.hitPanel == panel) { + const SDL_Event &sdlEvent = *pointerInput.iterator; + switch (sdlEvent.type) { + case SDL_MOUSEMOTION: { + egui::InputEvent evt; + auto &oevent = evt.pointerMoved; + oevent.type = egui::InputEventType::PointerMoved; + oevent.pos = egui::toPos2(pointerInput.panelCoord * virtualPointScale); + eguiInputTranslator.pushEvent(evt); + break; + } + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: { + egui::InputEvent evt; + auto &ievent = sdlEvent.button; + auto &oevent = evt.pointerButton; + oevent.type = egui::InputEventType::PointerButton; + oevent.pos = egui::toPos2(pointerInput.panelCoord * virtualPointScale); + oevent.pressed = sdlEvent.type == SDL_MOUSEBUTTONDOWN; + bool unknownButton = false; + switch (ievent.button) { + case SDL_BUTTON_LEFT: + oevent.button = egui::PointerButton::Primary; + break; + case SDL_BUTTON_MIDDLE: + oevent.button = egui::PointerButton::Middle; + break; + case SDL_BUTTON_RIGHT: + oevent.button = egui::PointerButton::Secondary; + break; + default: + unknownButton = true; + break; + } + if (unknownButton) + break; + eguiInputTranslator.pushEvent(evt); + break; + } + } + } + } + + if (panel == focusedPanel) { + for (auto &otherEvent : otherEvents) { + eguiInputTranslator.translateEvent(*otherEvent); + } + } + eguiInputTranslator.end(); + + auto geometry = panel->getGeometry(); + + egui::Input input = *eguiInputTranslator.getOutput(); + input.screenRect.min = egui::Pos2{}; + input.screenRect.max = egui::toPos2(geometry.size); + input.pixelsPerPoint = pixelsPerPoint; + + RenderContext ctx{ + .panel = panel, + .queue = queue, + .cached = getCachedPanel(panel), + .scaling = virtualPointScale, + .pixelsPerPoint = pixelsPerPoint, + }; + const egui::FullOutput &output = panel->render(input); + renderPanel(ctx, output); + } +} + +std::shared_ptr Context::getCachedPanel(PanelPtr panel) { + auto it = cachedPanels.find(panel.get()); + if (it == cachedPanels.end()) { + it = cachedPanels.insert(std::make_pair(panel.get(), std::make_shared())).first; + } + return it->second; +} + +} // namespace shards::vui diff --git a/src/virtual_ui/context.hpp b/src/virtual_ui/context.hpp new file mode 100644 index 0000000000..c8861fca85 --- /dev/null +++ b/src/virtual_ui/context.hpp @@ -0,0 +1,64 @@ +#ifndef AA53E1AF_30CD_452D_8285_B9C96CC68AD6 +#define AA53E1AF_30CD_452D_8285_B9C96CC68AD6 + +#include "panel.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gfx { +struct ShapeRenderer; +} + +namespace shards::vui { +typedef std::shared_ptr PanelPtr; +struct ContextCachedPanel; +struct Context { + std::vector panels; + std::map> cachedPanels; + + gfx::EguiInputTranslator eguiInputTranslator; + gfx::EguiRenderer eguiRenderer; + + PanelPtr lastFocusedPanel; + PanelPtr focusedPanel; + + // Points per world space unit + float virtualPointScale = 200.0f; + + // Resolution of rendered UI, in actual pixels per virtual UI pixel + float pixelsPerPoint = 2.0f; + + struct PointerInput { + input::InputBufferIterator iterator; + gfx::ViewRay ray; + + float hitDistance = FLT_MAX; + PanelPtr hitPanel; + + gfx::float2 panelCoord; + }; + std::vector pointerInputs; + std::vector otherEvents; + std::optional lastPointerInput; + + // Prepare input raycast + // inputToViewScale is used to convert coordinates from pointer events to view coordinates + void prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewScale, const gfx::SizedView &sizedView); + + void renderDebug(gfx::ShapeRenderer &sr); + + // Renders all the UI + void evaluate(gfx::DrawQueuePtr queue, double time, float deltaTime); + + std::shared_ptr getCachedPanel(PanelPtr panel); +}; +} // namespace shards::vui + +#endif /* AA53E1AF_30CD_452D_8285_B9C96CC68AD6 */ diff --git a/src/virtual_ui/panel.cpp b/src/virtual_ui/panel.cpp new file mode 100644 index 0000000000..0013ac6631 --- /dev/null +++ b/src/virtual_ui/panel.cpp @@ -0,0 +1,4 @@ +#include "panel.hpp" +using namespace gfx; +namespace shards::vui { +} // namespace shards::vui diff --git a/src/virtual_ui/panel.hpp b/src/virtual_ui/panel.hpp new file mode 100644 index 0000000000..a61b6e3713 --- /dev/null +++ b/src/virtual_ui/panel.hpp @@ -0,0 +1,39 @@ +#ifndef CA03D35E_D054_4C56_9F4B_949787F0B26F +#define CA03D35E_D054_4C56_9F4B_949787F0B26F + +#include +#include +#include + +namespace shards::vui { +struct PanelGeometry { + gfx::float3 anchor; + gfx::float3 center; + gfx::float3 up; + gfx::float3 right; + gfx::float2 size; + + PanelGeometry scaled(float scale) const { + return PanelGeometry{ + .anchor = anchor, + .center = center, + .up = up, + .right = right, + .size = size * scale, + }; + } + + gfx::float3 getTopLeft() const { + gfx::float2 halfSize = size * 0.5f; + return center - right * halfSize.x + up * halfSize.y; + } +}; + +struct Panel { + virtual ~Panel() = default; + virtual const egui::FullOutput &render(const egui::Input &inputs) = 0; + virtual PanelGeometry getGeometry() const = 0; +}; +} // namespace shards::vui + +#endif /* CA03D35E_D054_4C56_9F4B_949787F0B26F */ From 119738e09ce6e331ecb8a7edb9a89aae170701e2 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Tue, 17 Jan 2023 17:54:34 +0100 Subject: [PATCH 02/14] Add sort mode. Fix UI depth test. Add auto-resolution --- src/extra/gfx.cpp | 1 + src/extra/gfx/shards_types.hpp | 7 ++ src/extra/gfx/steps.cpp | 13 ++++ src/gfx/egui/egui_render_pass.hpp | 2 +- src/gfx/view_raycast.hpp | 1 + src/tests/vui.edn | 13 +++- src/virtual_ui/context.cpp | 102 +++++++++++++++++------------- src/virtual_ui/context.hpp | 16 +++++ src/virtual_ui/panel.hpp | 5 ++ 9 files changed, 113 insertions(+), 47 deletions(-) diff --git a/src/extra/gfx.cpp b/src/extra/gfx.cpp index 1db8a983ff..d53989f473 100644 --- a/src/extra/gfx.cpp +++ b/src/extra/gfx.cpp @@ -197,6 +197,7 @@ void registerShards() { REGISTER_ENUM(Types::CompareFunctionEnumInfo); REGISTER_ENUM(Types::ColorMaskEnumInfo); REGISTER_ENUM(Types::TextureTypeEnumInfo); + REGISTER_ENUM(Types::SortModeEnumInfo); registerMainWindowShards(); registerMeshShards(); diff --git a/src/extra/gfx/shards_types.hpp b/src/extra/gfx/shards_types.hpp index 19762ff3eb..edca0a9a91 100644 --- a/src/extra/gfx/shards_types.hpp +++ b/src/extra/gfx/shards_types.hpp @@ -156,6 +156,13 @@ struct Container { }; DECL_ENUM_INFO(TextureType_, TextureType, '_e9'); + enum class SortMode_ : uint8_t { + Batch = uint8_t(SortMode::Batch), + Queue = uint8_t(SortMode::Queue), + BackToFront = uint8_t(SortMode::BackToFront), + }; + DECL_ENUM_INFO(SortMode_, SortMode, '_e10'); + OBJECT('feat', "GFX.Feature", Feature, FeaturePtr) static inline Type FeatureSeq = Type::SeqOf(Feature); diff --git a/src/extra/gfx/steps.cpp b/src/extra/gfx/steps.cpp index 445b28b785..f1b8838be1 100644 --- a/src/extra/gfx/steps.cpp +++ b/src/extra/gfx/steps.cpp @@ -172,6 +172,7 @@ struct DrawablePassShard { :OutputScale (SHType::Float2 1.0 1.0) :Queue :Features [ ...] + :Sort } */ @@ -200,6 +201,11 @@ struct DrawablePassShard { step.drawQueue = *varAsObjectChecked(input, Types::DrawQueue); } + void applySorting(SHContext *context, RenderDrawablesStep &step, const SHVar &input) { + checkEnumType(input, Types::SortModeEnumInfo::Type, "DrawablePass Sort"); + step.sortMode = (gfx::SortMode)input.payload.enumValue; + } + SHVar activate(SHContext *context, const SHVar &input) { RenderDrawablesStep &step = std::get(*_step->get()); @@ -208,6 +214,13 @@ struct DrawablePassShard { shared::applyAll(context, step, inputTable); + SHVar sortVar; + if (getFromTable(context, inputTable, "Sort", sortVar)) + applySorting(context, step, sortVar); + else { + step.sortMode = SortMode::Batch; + } + SHVar queueVar; if (getFromTable(context, inputTable, "Queue", queueVar)) applyQueue(context, step, queueVar); diff --git a/src/gfx/egui/egui_render_pass.hpp b/src/gfx/egui/egui_render_pass.hpp index d774c1e930..86804b2f8e 100644 --- a/src/gfx/egui/egui_render_pass.hpp +++ b/src/gfx/egui/egui_render_pass.hpp @@ -70,7 +70,7 @@ fn egui_linear_from_srgb(srgb: vec3) -> vec3 { .alpha = BlendComponent::Opaque, }); feature->state.set_depthWrite(false); - feature->state.set_depthCompare(WGPUCompareFunction_Always); + feature->state.set_depthCompare(WGPUCompareFunction_Less); feature->state.set_culling(false); return feature; } diff --git a/src/gfx/view_raycast.hpp b/src/gfx/view_raycast.hpp index 7015d616bc..67c12b8c05 100644 --- a/src/gfx/view_raycast.hpp +++ b/src/gfx/view_raycast.hpp @@ -19,6 +19,7 @@ struct SizedView { float4x4 view; float4x4 viewInv; + SizedView() = default; SizedView(ViewPtr view, float2 size) : size(size) { viewProj = linalg::mul(view->getProjectionMatrix(size), view->view); viewProjInv = linalg::inverse(viewProj); diff --git a/src/tests/vui.edn b/src/tests/vui.edn index e99e2682b9..7b92dfcea3 100644 --- a/src/tests/vui.edn +++ b/src/tests/vui.edn @@ -13,7 +13,7 @@ ; Create render steps (GFX.BuiltinFeature :Id BuiltinFeatureId.Transform) >> .features (GFX.BuiltinFeature :Id BuiltinFeatureId.BaseColor) >> .features - {:Features .features :Queue .queue} (GFX.DrawablePass) >> .render-steps + {:Features .features :Queue .queue :Sort SortMode.Queue} (GFX.DrawablePass) >> .render-steps ; Initial panel transforms 15.0 (Math.DegreesToRadians) (Math.AxisAngleY) (Math.Rotation) >= .tmp @@ -58,7 +58,10 @@ "Wide panel" (UI.Label) (Setup (LoadImage "../../assets/ShardsLogo.png") (GFX.Texture) >= .button-texture) - .button-texture (UI.ImageButton :Scale (Float2 0.1)) + (UI.Horizontal (-> + .button-texture (UI.ImageButton :Scale (Float2 0.01)) + .button-texture (UI.ImageButton :Scale (Float2 0.01)) + .button-texture (UI.ImageButton :Scale (Float2 0.01)))) ; )))) ; @@ -66,6 +69,12 @@ .drawables (GFX.Draw .queue) + ;; (UI .queue (-> + ;; (UI.LeftPanel :Contents (-> + ;; "Left Panel" (UI.Label) + ;; (UI.Button :Label "Button"))))) + + (GFX.Render :Steps .render-steps :View .view)))) (schedule Root test-wire) diff --git a/src/virtual_ui/context.cpp b/src/virtual_ui/context.cpp index d2a5f6cf50..b489b51b51 100644 --- a/src/virtual_ui/context.cpp +++ b/src/virtual_ui/context.cpp @@ -12,6 +12,8 @@ using shards::input::ConsumeEventFilter; namespace shards::vui { void Context::prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewScale, const gfx::SizedView &sizedView) { + this->sizedView = sizedView; + pointerInputs.clear(); otherEvents.clear(); for (auto it = input.begin(); it; ++it) { @@ -132,33 +134,6 @@ struct RenderContext { float pixelsPerPoint; }; -static void renderPanel(const RenderContext &ctx, const egui::FullOutput &output) { - EguiRenderer &renderer = ctx.cached->renderer; - PanelPtr panel = ctx.panel; - - float uiToWorldScale = 1.0f / ctx.scaling; - - float4x4 rootTransform = linalg::identity; - PanelGeometry geom = panel->getGeometry().scaled(uiToWorldScale); - - // Geometry comes in in UI coordinates, and it's also scaled by pixelsPerPoint passed to egui - // so it needs to be scaled by `1.0f / (scale * pixelsPerPoint)` - rootTransform = linalg::mul(linalg::scaling_matrix(float3(1.0f / (ctx.scaling * ctx.pixelsPerPoint))), rootTransform); - - // Derive rotation matrix from panel right/up - float4x4 rotationMat; - rotationMat.x = float4(geom.right, 0); - rotationMat.y = float4(-geom.up, 0); // Render UI as using Y+ is down - rotationMat.z = float4(linalg::cross(geom.right, geom.up), 0); - rotationMat.w = float4(0, 0, 0, 1); - rootTransform = linalg::mul(rotationMat, rootTransform); - - // Place drawable at top-left corner of panel UI - rootTransform = linalg::mul(linalg::translation_matrix(geom.getTopLeft()), rootTransform); - - renderer.render(output, rootTransform, ctx.queue); -} - void Context::evaluate(gfx::DrawQueuePtr queue, double time, float deltaTime) { bool panelFocusChanged = lastFocusedPanel != focusedPanel; @@ -233,26 +208,65 @@ void Context::evaluate(gfx::DrawQueuePtr queue, double time, float deltaTime) { } } eguiInputTranslator.end(); - - auto geometry = panel->getGeometry(); - - egui::Input input = *eguiInputTranslator.getOutput(); - input.screenRect.min = egui::Pos2{}; - input.screenRect.max = egui::toPos2(geometry.size); - input.pixelsPerPoint = pixelsPerPoint; - - RenderContext ctx{ - .panel = panel, - .queue = queue, - .cached = getCachedPanel(panel), - .scaling = virtualPointScale, - .pixelsPerPoint = pixelsPerPoint, - }; - const egui::FullOutput &output = panel->render(input); - renderPanel(ctx, output); + renderPanel(queue, panel, *eguiInputTranslator.getOutput()); } } +// Check size of panel in view to determine resolution to render at +float Context::computeRenderResolution(PanelPtr panel) const { + PanelGeometry uiGeometry = panel->getGeometry(); + PanelGeometry worldGeometry = uiGeometry.scaled(1.0f / virtualPointScale); + + auto project = [&](float3 v) { + float4 tmp = linalg::mul(sizedView.viewProj, float4(v, 1.0f)); + return ((tmp.xyz() / tmp.w) * float3(0.5f) + float3(0.5f)) * float3(sizedView.size, 1.0f); + }; + + float3 projTL = project(worldGeometry.getTopLeft()); + float3 projTR = project(worldGeometry.getTopLeft() + worldGeometry.right * worldGeometry.size.x); + float sizeX = linalg::distance(projTL, projTR); + + float res = sizeX / uiGeometry.size.x; + res = std::round(res / resolutionGranularity) * resolutionGranularity; + return linalg::clamp(res, minResolution, maxResolution); +} + +void Context::renderPanel(gfx::DrawQueuePtr queue, PanelPtr panel, egui::Input input) { + auto geometry = panel->getGeometry(); + + input.screenRect.min = egui::Pos2{}; + input.screenRect.max = egui::toPos2(geometry.size); + + input.pixelsPerPoint = computeRenderResolution(panel); + + const egui::FullOutput &output = panel->render(input); + + std::shared_ptr cachedPanel = getCachedPanel(panel); + EguiRenderer &renderer = cachedPanel->renderer; + + float uiToWorldScale = 1.0f / virtualPointScale; + + float4x4 rootTransform = linalg::identity; + PanelGeometry geom = panel->getGeometry().scaled(uiToWorldScale); + + // Geometry comes in in UI coordinates, and it's also scaled by pixelsPerPoint passed to egui + // so it needs to be scaled by `1.0f / (scale * pixelsPerPoint)` + rootTransform = linalg::mul(linalg::scaling_matrix(float3(1.0f / (virtualPointScale * input.pixelsPerPoint))), rootTransform); + + // Derive rotation matrix from panel right/up + float4x4 rotationMat; + rotationMat.x = float4(geom.right, 0); + rotationMat.y = float4(-geom.up, 0); // Render UI as using Y+ is down + rotationMat.z = float4(linalg::cross(geom.right, geom.up), 0); + rotationMat.w = float4(0, 0, 0, 1); + rootTransform = linalg::mul(rotationMat, rootTransform); + + // Place drawable at top-left corner of panel UI + rootTransform = linalg::mul(linalg::translation_matrix(geom.getTopLeft()), rootTransform); + + renderer.render(output, rootTransform, queue); +} + std::shared_ptr Context::getCachedPanel(PanelPtr panel) { auto it = cachedPanels.find(panel.get()); if (it == cachedPanels.end()) { diff --git a/src/virtual_ui/context.hpp b/src/virtual_ui/context.hpp index c8861fca85..63b152e341 100644 --- a/src/virtual_ui/context.hpp +++ b/src/virtual_ui/context.hpp @@ -29,12 +29,23 @@ struct Context { PanelPtr lastFocusedPanel; PanelPtr focusedPanel; + gfx::SizedView sizedView; + // Points per world space unit float virtualPointScale = 200.0f; // Resolution of rendered UI, in actual pixels per virtual UI pixel float pixelsPerPoint = 2.0f; + // Minimum resolution to render UI at from a distance + const float minResolution = 0.25f; + + // Maxmimum resolution to render UI at up close + const float maxResolution = 8.0f; + + // Steps that resolution switches at + const float resolutionGranularity = 0.25f; + struct PointerInput { input::InputBufferIterator iterator; gfx::ViewRay ray; @@ -57,7 +68,12 @@ struct Context { // Renders all the UI void evaluate(gfx::DrawQueuePtr queue, double time, float deltaTime); +private: std::shared_ptr getCachedPanel(PanelPtr panel); + + void renderPanel(gfx::DrawQueuePtr queue, PanelPtr panel, egui::Input baseInput); + + float computeRenderResolution(PanelPtr panel) const; }; } // namespace shards::vui diff --git a/src/virtual_ui/panel.hpp b/src/virtual_ui/panel.hpp index a61b6e3713..5bbb58299a 100644 --- a/src/virtual_ui/panel.hpp +++ b/src/virtual_ui/panel.hpp @@ -27,6 +27,11 @@ struct PanelGeometry { gfx::float2 halfSize = size * 0.5f; return center - right * halfSize.x + up * halfSize.y; } + + gfx::float3 getBottomRight() const { + gfx::float2 halfSize = size * 0.5f; + return center + right * halfSize.x - up * halfSize.y; + } }; struct Panel { From 3011aa41478a6594b061708eacfc78be11fde8da Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Tue, 17 Jan 2023 18:11:24 +0100 Subject: [PATCH 03/14] Mix VUI with regular UI --- src/extra/CMakeLists.txt | 2 +- src/extra/egui/context.cpp | 1 + src/extra/{gui.cpp => egui/pass.cpp} | 13 +++-- src/extra/runtime.cpp | 6 ++- src/gfx/egui/egui_render_pass.hpp | 6 ++- src/tests/vui.edn | 76 +++++++++++++++------------- src/virtual_ui/context.cpp | 1 - 7 files changed, 58 insertions(+), 47 deletions(-) create mode 100644 src/extra/egui/context.cpp rename src/extra/{gui.cpp => egui/pass.cpp} (84%) diff --git a/src/extra/CMakeLists.txt b/src/extra/CMakeLists.txt index b4015f3389..19cef97b19 100644 --- a/src/extra/CMakeLists.txt +++ b/src/extra/CMakeLists.txt @@ -20,7 +20,7 @@ set(extra_SOURCES gfx/view.cpp gfx/steps.cpp vui/vui.cpp - gui.cpp + egui/pass.cpp rust_interop.cpp gizmos/context.cpp gizmos/gizmos.cpp diff --git a/src/extra/egui/context.cpp b/src/extra/egui/context.cpp new file mode 100644 index 0000000000..e8ad4b7e1f --- /dev/null +++ b/src/extra/egui/context.cpp @@ -0,0 +1 @@ +#include "context.cpp" \ No newline at end of file diff --git a/src/extra/gui.cpp b/src/extra/egui/pass.cpp similarity index 84% rename from src/extra/gui.cpp rename to src/extra/egui/pass.cpp index 8baf0210d7..808b94e35d 100644 --- a/src/extra/gui.cpp +++ b/src/extra/egui/pass.cpp @@ -1,20 +1,20 @@ -#include "gfx/shards_types.hpp" +#include "../gfx/shards_types.hpp" #include #include #include #include -#include "gfx/egui/egui_render_pass.hpp" +#include using namespace gfx; -namespace shards { -namespace Gui { +namespace shards::egui { using gfx::Types; +using shards::CoreInfo; struct UIPassShard { static SHTypesInfo inputTypes() { return CoreInfo::NoneType; } - static SHTypesInfo outputTypes() { return Types::PipelineStep; } + static SHTypesInfo outputTypes() { return gfx::Types::PipelineStep; } PARAM_PARAMVAR(_queue, "Queue", "The queue to draw from (Optional). Uses the default queue if not specified", {CoreInfo::NoneType, Type::VariableOf(Types::DrawQueue)}); @@ -48,5 +48,4 @@ struct UIPassShard { }; void registerShards() { REGISTER_SHARD("GFX.UIPass", UIPassShard); } -} // namespace Gui -} // namespace shards \ No newline at end of file +} // namespace shards::egui \ No newline at end of file diff --git a/src/extra/runtime.cpp b/src/extra/runtime.cpp index efafd559e0..5f8a134b31 100644 --- a/src/extra/runtime.cpp +++ b/src/extra/runtime.cpp @@ -46,6 +46,10 @@ namespace Gizmos { extern void registerShards(); } +namespace egui { +extern void registerShards(); +} + namespace VUI { extern void registerShards(); } @@ -63,7 +67,7 @@ void shInitExtras() { Inputs::registerShards(); Audio::registerShards(); DSP::registerShards(); - Gui::registerShards(); + egui::registerShards(); VUI::registerShards(); #ifdef _WIN32 diff --git a/src/gfx/egui/egui_render_pass.hpp b/src/gfx/egui/egui_render_pass.hpp index 86804b2f8e..70b893a8b5 100644 --- a/src/gfx/egui/egui_render_pass.hpp +++ b/src/gfx/egui/egui_render_pass.hpp @@ -20,7 +20,10 @@ struct EguiTransformFeature { code->appendLine("var p4 = vec4(p1.xy, 0.0, 1.0)"); code->appendLine(WriteOutput("position", shader::FieldTypes::Float4, "p4")); - feature->shaderEntryPoints.emplace_back("uiBaseTransform", ProgrammableGraphicsStage::Vertex, std::move(code)); + auto &transform = + feature->shaderEntryPoints.emplace_back("writeUiTransform", ProgrammableGraphicsStage::Vertex, std::move(code)); + // NOTE: Override Transform's writePosition + transform.dependencies.emplace_back("writePosition"); return feature; } @@ -84,7 +87,6 @@ struct EguiRenderPass { .features = std::vector{ EguiTransformFeature::create(), - EguiColorFeature::create(), }, }); return drawableStep; diff --git a/src/tests/vui.edn b/src/tests/vui.edn index 7b92dfcea3..1499369d3a 100644 --- a/src/tests/vui.edn +++ b/src/tests/vui.edn @@ -5,6 +5,7 @@ 0.0 >= .time (GFX.DrawQueue) >= .queue + (GFX.DrawQueue) >= .screen-ui-queue (GFX.BuiltinMesh :Type BuiltinMeshType.Cube) >= .mesh (Float3 0 0 0) (Math.Translation) >= .transform-0 @@ -14,6 +15,7 @@ (GFX.BuiltinFeature :Id BuiltinFeatureId.Transform) >> .features (GFX.BuiltinFeature :Id BuiltinFeatureId.BaseColor) >> .features {:Features .features :Queue .queue :Sort SortMode.Queue} (GFX.DrawablePass) >> .render-steps + (GFX.UIPass .screen-ui-queue) >> .render-steps ; Initial panel transforms 15.0 (Math.DegreesToRadians) (Math.AxisAngleY) (Math.Rotation) >= .tmp @@ -36,44 +38,48 @@ .view-transform (FreeCamera :FlySpeed 10.0) > .view-transform .queue (GFX.ClearQueue) - (VUI.Context :Queue .queue :View .view :Scale 100.0 :Contents - (-> - (VUI.Panel :Transform .panel-t-0 :Size (Float2 100 100) :Contents - (-> - (UI.CentralPanel (-> - "First panel" (UI.Label) - (UI.Button :Label "Button") - ; - )))) - (VUI.Panel :Transform .panel-t-1 :Size (Float2 100 100) :Contents - (-> - (UI.CentralPanel (-> - "Some other panel" (UI.Label) - (UI.Button :Label "Button") - ; - )))) - (VUI.Panel :Transform .panel-t-2 :Size (Float2 300 60) :Contents - (-> - (UI.CentralPanel (-> - "Wide panel" (UI.Label) - (Setup - (LoadImage "../../assets/ShardsLogo.png") (GFX.Texture) >= .button-texture) - (UI.Horizontal (-> - .button-texture (UI.ImageButton :Scale (Float2 0.01)) - .button-texture (UI.ImageButton :Scale (Float2 0.01)) - .button-texture (UI.ImageButton :Scale (Float2 0.01)))) - ; - )))) - ; - )) + (VUI.Context + :Queue .queue :View .view :Scale 100.0 :Contents + (-> + (VUI.Panel + :Transform .panel-t-0 :Size (Float2 100 100) :Contents + (-> + (UI.CentralPanel + (-> + "First panel" (UI.Label) + (UI.Button :Label "Button"))))) + (VUI.Panel + :Transform .panel-t-1 :Size (Float2 100 100) :Contents + (-> + (UI.CentralPanel + (-> + "Some other panel" (UI.Label) + (UI.Button :Label "Button"))))) + (VUI.Panel + :Transform .panel-t-2 :Size (Float2 300 60) :Contents + (-> + (UI.CentralPanel + (-> + "Wide panel" (UI.Label) + (Setup + (LoadImage "../../assets/ShardsLogo.png") (GFX.Texture) >= .button-texture) + (UI.Horizontal (-> + .button-texture (UI.ImageButton :Scale (Float2 0.01)) + .button-texture (UI.ImageButton :Scale (Float2 0.01)) + .button-texture (UI.ImageButton :Scale (Float2 0.01)))))))) + ; + )) .drawables (GFX.Draw .queue) - ;; (UI .queue (-> - ;; (UI.LeftPanel :Contents (-> - ;; "Left Panel" (UI.Label) - ;; (UI.Button :Label "Button"))))) - + .screen-ui-queue (GFX.ClearQueue) + (UI .screen-ui-queue + (-> + (UI.LeftPanel + :Contents + (-> + "Left Panel" (UI.Label) + (UI.Button :Label "Button"))))) (GFX.Render :Steps .render-steps :View .view)))) diff --git a/src/virtual_ui/context.cpp b/src/virtual_ui/context.cpp index b489b51b51..b3b748e2dc 100644 --- a/src/virtual_ui/context.cpp +++ b/src/virtual_ui/context.cpp @@ -8,7 +8,6 @@ #include using namespace gfx; -using shards::input::ConsumeEventFilter; namespace shards::vui { void Context::prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewScale, const gfx::SizedView &sizedView) { From e3b70a3b3cfc2dcb9dc5e1cbc1b24131707db433 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Tue, 17 Jan 2023 18:56:06 +0100 Subject: [PATCH 04/14] Add VUI docs --- docs/details/shards/VUI/Context.md | 10 ++++ docs/details/shards/VUI/Panel.md | 10 ++++ .../shards/VUI/assets/render_output.png | Bin 0 -> 25223 bytes docs/samples/shards/VUI/Context/1.edn | 55 ++++++++++++++++++ docs/samples/shards/VUI/Context/1.out.md | 1 + 5 files changed, 76 insertions(+) create mode 100644 docs/details/shards/VUI/Context.md create mode 100644 docs/details/shards/VUI/Panel.md create mode 100644 docs/docs/reference/shards/VUI/assets/render_output.png create mode 100644 docs/samples/shards/VUI/Context/1.edn create mode 100644 docs/samples/shards/VUI/Context/1.out.md diff --git a/docs/details/shards/VUI/Context.md b/docs/details/shards/VUI/Context.md new file mode 100644 index 0000000000..df44997153 --- /dev/null +++ b/docs/details/shards/VUI/Context.md @@ -0,0 +1,10 @@ +This shard defines the starting point for creating interactive world space UI elements. + +Every instance of [VUI.Panel](../Panel) should be placed within this shards's Contents parameter. + +The draw commands for the UI elements are added to the Queue that is passed in. In contrast to the screen space [UI](../../General/UI) shard, they do not need to be rendered using a [GFX.UIPass](../../GFX/UIPass) + +!!! note + + When rendering the draw commands, the sorting mode should be set to [SortMode.Queue](../../../enums/SortMode) so they are drawn in the correct order. + The [BuiltinFeatureId.Transform](../../GFX/BuiltinFeature/#transform) feature should be used diff --git a/docs/details/shards/VUI/Panel.md b/docs/details/shards/VUI/Panel.md new file mode 100644 index 0000000000..8361f2a6a5 --- /dev/null +++ b/docs/details/shards/VUI/Panel.md @@ -0,0 +1,10 @@ +This shards defines a single world space UI panel. This shard needs to be placed within a [VUI.Context](../Context)'s Contents block. + +This size of the panels defined using virtual UI points/units/pixels. How these virtual points map to world coordinates is controlled by the Context's Scale parameter. For example if the Context's scale is set to 200; 200 virtual points will be equal to 1 unit in world space. + +A panel's position and orientation in the world is controlled by the `Transform` parameter, which is a standard 4x4 transformation matrix. + +!!! danger + Applying scaling on this transform is not recommended and may result in undefined behavior. + +The Panels' contents should be placed in the `Contents` parameter, similar to the screen space [UI](../../General/UI) \ No newline at end of file diff --git a/docs/docs/reference/shards/VUI/assets/render_output.png b/docs/docs/reference/shards/VUI/assets/render_output.png new file mode 100644 index 0000000000000000000000000000000000000000..6d81bd1ba5f1494f0866e7afbac141d217750f34 GIT binary patch literal 25223 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 PMakB3fknFcs$G8p7e*^h literal 0 HcmV?d00001 diff --git a/docs/samples/shards/VUI/Context/1.edn b/docs/samples/shards/VUI/Context/1.edn new file mode 100644 index 0000000000..18d1c9200f --- /dev/null +++ b/docs/samples/shards/VUI/Context/1.edn @@ -0,0 +1,55 @@ +(Setup + (GFX.DrawQueue) >= .queue + + ; Create render steps + (GFX.BuiltinFeature :Id BuiltinFeatureId.Transform) >> .features + {:Features .features :Queue .queue :Sort SortMode.Queue} (GFX.DrawablePass) >> .render-steps + + ; Panel transforms + 15.0 (Math.DegreesToRadians) (Math.AxisAngleY) (Math.Rotation) >= .tmp + (Float3 -1.0 0.0 0.0) (Math.Translation) (Math.MatMul .tmp) >= .panel-t-0 + -15.0 (Math.DegreesToRadians) (Math.AxisAngleY) (Math.Rotation) >= .tmp + (Float3 1.0 0.0 0.0) (Math.Translation) (Math.MatMul .tmp) >= .panel-t-1 + 5.0 (Math.DegreesToRadians) (Math.AxisAngleX) (Math.Rotation) >= .tmp + (Float3 0.0 1.2 0.0) (Math.Translation) (Math.MatMul .tmp) >= .panel-t-2 + + ; Camera + {:Position (Float3 1 2 8) :Target (Float3 0 0 0)} (Math.LookAt) >= .view-transform + (GFX.View :View .view-transform) >= .view) + +(GFX.MainWindow + :Title "World space UI" :Width 1280 :Height 720 + :Contents + (-> + .queue (GFX.ClearQueue) + (VUI.Context + :Queue .queue :View .view :Scale 100.0 :Contents + (-> + (VUI.Panel + :Transform .panel-t-0 :Size (Float2 100 100) :Contents + (-> + (UI.CentralPanel + (-> + "Left panel" (UI.Label) + (UI.Button :Label "Button"))))) + (VUI.Panel + :Transform .panel-t-1 :Size (Float2 100 100) :Contents + (-> + (UI.CentralPanel + (-> + "Right panel" (UI.Label) + (UI.Button :Label "Button"))))) + (VUI.Panel + :Transform .panel-t-2 :Size (Float2 300 60) :Contents + (-> + (UI.CentralPanel + (-> + "Wide panel" (UI.Label) + (Setup + (LoadImage "../../assets/ShardsLogo.png") (GFX.Texture) >= .button-texture) + (UI.Horizontal (-> + .button-texture (UI.ImageButton :Scale (Float2 0.01)) + .button-texture (UI.ImageButton :Scale (Float2 0.01)) + .button-texture (UI.ImageButton :Scale (Float2 0.01)))))))))) + + (GFX.Render :Steps .render-steps :View .view))) diff --git a/docs/samples/shards/VUI/Context/1.out.md b/docs/samples/shards/VUI/Context/1.out.md new file mode 100644 index 0000000000..00cddfda77 --- /dev/null +++ b/docs/samples/shards/VUI/Context/1.out.md @@ -0,0 +1 @@ +![Image](../assets/render_output.png) From 4613129ea3bc65e826e1eca83425758e96515cee Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Tue, 17 Jan 2023 19:47:50 +0100 Subject: [PATCH 05/14] Fix missing include --- src/virtual_ui/context.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/virtual_ui/context.hpp b/src/virtual_ui/context.hpp index 63b152e341..9b3d0a8db0 100644 --- a/src/virtual_ui/context.hpp +++ b/src/virtual_ui/context.hpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace gfx { struct ShapeRenderer; From 31ccb8253403ab30c78e8ba95295ee97b1ed85cb Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Wed, 18 Jan 2023 09:57:11 +0100 Subject: [PATCH 06/14] Limit Feature lifetime to shard --- src/extra/gfx/feature.cpp | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/extra/gfx/feature.cpp b/src/extra/gfx/feature.cpp index 0f86460494..707e20932a 100644 --- a/src/extra/gfx/feature.cpp +++ b/src/extra/gfx/feature.cpp @@ -112,6 +112,9 @@ struct FeatureShard { IterableExposedInfo _sharedCopy; + FeaturePtr *_featurePtr{}; + SHVar _variable{}; + void setParam(int index, const SHVar &value) { switch (index) { default: @@ -126,8 +129,19 @@ struct FeatureShard { } } - void cleanup() {} - void warmup(SHContext *context) {} + void cleanup() { + if (_featurePtr) { + clear(); + Types::FeatureObjectVar.Release(_featurePtr); + _featurePtr = nullptr; + } + } + + void warmup(SHContext *context) { + _featurePtr = Types::FeatureObjectVar.New(); + *_featurePtr = std::make_shared(); + } + SHTypeInfo compose(const SHInstanceData &data) { // Capture variables for callbacks // TODO: Refactor IterableArray @@ -454,11 +468,19 @@ struct FeatureShard { ForEach(inputSeq, [&](SHVar v) { applyParam(context, feature, v); }); } + void clear() { + Feature &feature = *_featurePtr->get(); + feature.drawableParameterGenerators.clear(); + feature.shaderEntryPoints.clear(); + feature.shaderParams.clear(); + feature.viewParameterGenerators.clear(); + } + SHVar activate(SHContext *context, const SHVar &input) { - FeaturePtr *varPtr = Types::FeatureObjectVar.New(); - *varPtr = std::make_shared(); + Feature &feature = *_featurePtr->get(); - Feature &feature = *varPtr->get(); + // Reset feature first + clear(); checkType(input.valueType, SHType::Table, "Input table"); const SHTable &inputTable = input.payload.tableValue; @@ -479,7 +501,7 @@ struct FeatureShard { if (getFromTable(context, inputTable, "Params", paramsVar)) applyParams(context, feature, paramsVar); - return Types::FeatureObjectVar.Get(varPtr); + return Types::FeatureObjectVar.Get(_featurePtr); } }; From d98691b436bdd0148d043fdd6a69b17cce6e941c Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Thu, 19 Jan 2023 13:12:44 +0100 Subject: [PATCH 07/14] Various fixes & cleanup --- src/extra/gizmos/context.cpp | 4 ++-- src/extra/vui/vui.cpp | 18 ++---------------- src/gfx/egui/input.cpp | 6 +++--- src/gfx/egui/renderer.cpp | 11 +++++++---- src/gfx/egui/renderer.hpp | 3 ++- src/gfx/window.cpp | 4 +++- src/gfx/window.hpp | 3 +++ src/virtual_ui/context.cpp | 20 ++++++++------------ src/virtual_ui/context.hpp | 8 ++++++-- 9 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/extra/gizmos/context.cpp b/src/extra/gizmos/context.cpp index b36b98a877..e73251eb57 100644 --- a/src/extra/gizmos/context.cpp +++ b/src/extra/gizmos/context.cpp @@ -108,10 +108,10 @@ struct GizmosContextShard { handleGizmoInputEvents(_inputContext->events); - float2 drawableScale = float2(window.getDrawableSize()) / float2(window.getSize()); + float2 inputScale = window.getInputScale(); gfx::gizmos::InputState gizmoInput; - gizmoInput.cursorPosition = float2(_cursorPosition) * drawableScale; + gizmoInput.cursorPosition = float2(_cursorPosition) * inputScale; gizmoInput.pressed = _mouseButtonState; gizmoInput.viewSize = float2(outputSize); diff --git a/src/extra/vui/vui.cpp b/src/extra/vui/vui.cpp index d2b9888747..f01cf818cd 100644 --- a/src/extra/vui/vui.cpp +++ b/src/extra/vui/vui.cpp @@ -41,7 +41,7 @@ struct VUIContextShard { VUIContextShard() { _scale = Var(1000.0f); - _debug = Var(true); + _debug = Var(false); } static SHTypesInfo inputTypes() { return CoreInfo::NoneType; } @@ -105,9 +105,8 @@ struct VUIContextShard { _vuiContext.activationContext = shContext; _vuiContext.context.virtualPointScale = _scale.payload.floatValue; withObjectVariable(*_vuiContextVar, &_vuiContext, VUIContext::Type, [&]() { - gfx::float2 inputToViewScale{1.0f}; gfx::SizedView sizedView(view.view, gfx::float2(viewStackTop.viewport.getSize())); - _vuiContext.context.prepareInputs(_inputBuffer, inputToViewScale, sizedView); + _vuiContext.context.prepareInputs(_inputBuffer, _graphicsContext->window->getInputScale(), sizedView); _vuiContext.context.evaluate(queue.queue, _graphicsContext->time, _graphicsContext->deltaTime); }); _vuiContext.activationContext = nullptr; @@ -235,19 +234,6 @@ void VUIContextShard::warmup(SHContext *context) { _graphicsContext.warmup(context); withObjectVariable(*_vuiContextVar, &_vuiContext, VUIContext::Type, [&]() { PARAM_WARMUP(context); }); - - // Collect VUI.Panel shards similar to UI dock does - // _panels.clear(); - // auto contentShards = _contents.shards(); - // for (size_t i = 0; i < contentShards.len; i++) { - // ShardPtr shard = contentShards.elements[i]; - - // if (std::string(VUI_PANEL_SHARD_NAME) == shard->name(shard)) { - // using ShardWrapper = shards::ShardWrapper; - // VUIPanelShard &vuiPanel = reinterpret_cast(shard)->shard; - // _panels.emplace_back(&vuiPanel); - // } - // } } void registerShards() { diff --git a/src/gfx/egui/input.cpp b/src/gfx/egui/input.cpp index c3b1fcde49..adaab1b9a8 100644 --- a/src/gfx/egui/input.cpp +++ b/src/gfx/egui/input.cpp @@ -70,12 +70,12 @@ void EguiInputTranslator::setupWindowInput(Window &window, int4 mappedWindowRegi float eguiDrawScale = EguiRenderer::getDrawScale(window) * scalingFactor; // Drawable/Window scale - float2 drawableScale = float2(window.getDrawableSize()) / float2(window.getSize()); - windowToEguiScale = drawableScale / eguiDrawScale; + float2 inputScale = window.getInputScale(); + windowToEguiScale = inputScale / eguiDrawScale; // Convert from pixel to window coordinates this->mappedWindowRegion = - int4(float4(mappedWindowRegion) / float4(drawableScale.x, drawableScale.y, drawableScale.x, drawableScale.y)); + int4(float4(mappedWindowRegion) / float4(inputScale.x, inputScale.y, inputScale.x, inputScale.y)); // Take viewport size and scale it by the draw scale float2 viewportSizeFloat = float2(float(viewportSize.x), float(viewportSize.y)); diff --git a/src/gfx/egui/renderer.cpp b/src/gfx/egui/renderer.cpp index ced359dc5e..0b68d85282 100644 --- a/src/gfx/egui/renderer.cpp +++ b/src/gfx/egui/renderer.cpp @@ -169,7 +169,8 @@ struct EguiRendererImpl { pendingTextureFrees.clear(); } - void render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue) { + void render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue, + bool clipGeometry) { meshPool.reset(); processPendingTextureFrees(); @@ -202,7 +203,8 @@ struct EguiRendererImpl { } } - // drawable.clipRect = int4(prim.clipRect.min.x, prim.clipRect.min.y, prim.clipRect.max.x, prim.clipRect.max.y); + if (clipGeometry) + drawable.clipRect = int4(prim.clipRect.min.x, prim.clipRect.min.y, prim.clipRect.max.x, prim.clipRect.max.y); drawQueue->add(drawable); } @@ -218,8 +220,9 @@ struct EguiRendererImpl { EguiRenderer::EguiRenderer() { impl = std::make_shared(); } -void EguiRenderer::render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue) { - impl->render(output, rootTransform, drawQueue); +void EguiRenderer::render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue, + bool clipGeometry) { + impl->render(output, rootTransform, drawQueue, clipGeometry); } void EguiRenderer::renderNoTransform(const egui::FullOutput &output, const gfx::DrawQueuePtr &drawQueue) { diff --git a/src/gfx/egui/renderer.hpp b/src/gfx/egui/renderer.hpp index 56d7de3bec..79934d66b5 100644 --- a/src/gfx/egui/renderer.hpp +++ b/src/gfx/egui/renderer.hpp @@ -16,7 +16,8 @@ struct EguiRenderer { EguiRenderer(); - void render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue); + // clipGeometry controls settings of screen space clip rectangles based on egui + void render(const egui::FullOutput &output, const float4x4 &rootTransform, const gfx::DrawQueuePtr &drawQueue, bool clipGeometry = true); void renderNoTransform(const egui::FullOutput &output, const gfx::DrawQueuePtr &drawQueue); static EguiRenderer *create(); diff --git a/src/gfx/window.cpp b/src/gfx/window.cpp index 25e776037b..a328560e95 100644 --- a/src/gfx/window.cpp +++ b/src/gfx/window.cpp @@ -93,6 +93,8 @@ int2 Window::getDrawableSize() const { return r; } +float2 Window::getInputScale() const { return float2(getDrawableSize()) / float2(getSize()); } + int2 Window::getSize() const { int2 r; SDL_GetWindowSize(window, &r.x, &r.y); @@ -118,7 +120,7 @@ static float2 getUIScaleFromDisplayDPI(int displayIndex) { float2 Window::getUIScale() const { #if GFX_APPLE - // On apply, derive display scale from drawable size / window size + // On apple, derive display scale from drawable size / window size return float2(getDrawableSize()) / float2(getSize()); #else return getUIScaleFromDisplayDPI(SDL_GetWindowDisplayIndex(window)); diff --git a/src/gfx/window.hpp b/src/gfx/window.hpp index 977b232f8b..e093aa360d 100644 --- a/src/gfx/window.hpp +++ b/src/gfx/window.hpp @@ -37,6 +37,9 @@ struct Window { // draw surface size int2 getDrawableSize() const; + // The scale that converts from input coordinates to drawable surface coordinates + float2 getInputScale() const; + // window size int2 getSize() const; diff --git a/src/virtual_ui/context.cpp b/src/virtual_ui/context.cpp index b3b748e2dc..5dc8281d62 100644 --- a/src/virtual_ui/context.cpp +++ b/src/virtual_ui/context.cpp @@ -10,6 +10,11 @@ using namespace gfx; namespace shards::vui { +// Associated panel data +struct ContextCachedPanel { + gfx::EguiRenderer renderer; +}; + void Context::prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewScale, const gfx::SizedView &sizedView) { this->sizedView = sizedView; @@ -40,12 +45,6 @@ void Context::prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewSc } } - struct PanelInput { - std::vector inputEvents; - }; - - std::vector panelEvents; - float uiToWorldScale = 1.0f / virtualPointScale; lastFocusedPanel = focusedPanel; @@ -97,7 +96,6 @@ void Context::renderDebug(gfx::ShapeRenderer &sr) { auto geom = panel->getGeometry().scaled(uiToWorldScale); float4 c = panel == focusedPanel ? float4(0, 1, 0, 1) : float4(0.8, 0.8, 0.8, 1.0); sr.addRect(geom.center, geom.right, geom.up, geom.size, c, 2); - // panel->transform } bool pointerEventVisualized = false; @@ -121,10 +119,6 @@ void Context::renderDebug(gfx::ShapeRenderer &sr) { } } -struct ContextCachedPanel { - gfx::EguiRenderer renderer; -}; - struct RenderContext { PanelPtr panel; gfx::DrawQueuePtr queue; @@ -263,7 +257,9 @@ void Context::renderPanel(gfx::DrawQueuePtr queue, PanelPtr panel, egui::Input i // Place drawable at top-left corner of panel UI rootTransform = linalg::mul(linalg::translation_matrix(geom.getTopLeft()), rootTransform); - renderer.render(output, rootTransform, queue); + // NOTE: clipping disabled since we're rendering in world space + // TODO(guusw): geometry needs to be clipped using stencil or rendered to texture first + renderer.render(output, rootTransform, queue, false); } std::shared_ptr Context::getCachedPanel(PanelPtr panel) { diff --git a/src/virtual_ui/context.hpp b/src/virtual_ui/context.hpp index 9b3d0a8db0..2643a63b93 100644 --- a/src/virtual_ui/context.hpp +++ b/src/virtual_ui/context.hpp @@ -61,19 +61,23 @@ struct Context { std::optional lastPointerInput; // Prepare input raycast + // needs to be called before evaluate // inputToViewScale is used to convert coordinates from pointer events to view coordinates void prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewScale, const gfx::SizedView &sizedView); - void renderDebug(gfx::ShapeRenderer &sr); - // Renders all the UI + // prepareInputs needs to be called before evaluate void evaluate(gfx::DrawQueuePtr queue, double time, float deltaTime); + // Render debug vizualizations to the target shape renderer + void renderDebug(gfx::ShapeRenderer &sr); + private: std::shared_ptr getCachedPanel(PanelPtr panel); void renderPanel(gfx::DrawQueuePtr queue, PanelPtr panel, egui::Input baseInput); + // Computes the render scale to render the given panel at, taking it's projected screen area into account float computeRenderResolution(PanelPtr panel) const; }; } // namespace shards::vui From 19f221765c959969830c345ab1dc5f511bbea7ef Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Fri, 20 Jan 2023 22:04:34 +0100 Subject: [PATCH 08/14] Rename raw egui context => EguiHost. Use in UI shard --- rust/src/shards/gui/context.rs | 142 ++++-------------- .../gui/{raw_context.rs => egui_host.rs} | 42 ++++-- rust/src/shards/gui/mod.rs | 8 +- rust/src/types.rs | 32 ++-- src/extra/egui/context.hpp | 17 ++- src/extra/vui/vui.cpp | 28 ++-- src/gfx/egui/src/renderer.rs | 18 ++- src/tests/vui.edn | 2 +- 8 files changed, 116 insertions(+), 173 deletions(-) rename rust/src/shards/gui/{raw_context.rs => egui_host.rs} (86%) diff --git a/rust/src/shards/gui/context.rs b/rust/src/shards/gui/context.rs index 7a5997f848..b519b9128c 100644 --- a/rust/src/shards/gui/context.rs +++ b/rust/src/shards/gui/context.rs @@ -1,15 +1,18 @@ /* SPDX-License-Identifier: BSD-3-Clause */ /* Copyright © 2022 Fragcolor Pte. Ltd. */ +use egui_gfx::gfx_EguiRenderer_renderNoTransform; + +use super::egui_host::EguiHost; use super::util; use super::EguiContext; use super::CONTEXTS_NAME; use super::EGUI_CTX_TYPE; use super::EGUI_UI_SEQ_TYPE; use super::GFX_CONTEXT_TYPE; -use super::INPUT_CONTEXT_TYPE; use super::GFX_QUEUE_VAR_TYPES; use super::HELP_OUTPUT_EQUAL_INPUT; +use super::INPUT_CONTEXT_TYPE; use super::PARENTS_UI_NAME; use crate::shard::Shard; use crate::shardsc; @@ -56,8 +59,7 @@ impl Default for EguiContext { parents.set_name(PARENTS_UI_NAME); Self { - context: None, - instance: ctx, + host: EguiHost::default(), requiring: Vec::new(), queue: ParamVar::default(), contents: ShardsVar::default(), @@ -73,7 +75,6 @@ impl Default for EguiContext { var.set_name(CStr::from_ptr(name).to_str().unwrap()); var }, - parents, renderer: egui_gfx::Renderer::new(), input_translator: egui_gfx::InputTranslator::new(), } @@ -168,30 +169,12 @@ impl Shard for EguiContext { let mut data = *data; // clone shared into a new vector we can append things to let mut shared: ExposedTypes = data.shared.into(); + // expose UI context - let ctx_info = ExposedInfo { - exposedType: EGUI_CTX_TYPE, - name: self.instance.get_name(), - help: cstr!("The UI context.").into(), - isMutable: false, - isProtected: true, // don't allow to be used in code/wires - isTableEntry: false, - global: false, - isPushTable: false, - }; - shared.push(ctx_info); - // expose UI parents seq - let ui_info = ExposedInfo { - exposedType: EGUI_UI_SEQ_TYPE, - name: self.parents.get_name(), - help: cstr!("The parent UI objects.").into(), - isMutable: false, - isProtected: true, // don't allow to be used in code/wires - isTableEntry: false, - global: false, - isPushTable: false, - }; - shared.push(ui_info); + for exposed in self.host.get_exposed_info() { + shared.push(*exposed); + } + // update shared data.shared = (&shared).into(); @@ -205,120 +188,51 @@ impl Shard for EguiContext { } fn warmup(&mut self, ctx: &Context) -> Result<(), &str> { - self.context = Some(egui::Context::default()); - self.instance.warmup(ctx); + self.host.warmup(ctx)?; self.queue.warmup(ctx); self.contents.warmup(ctx)?; self.graphics_context.warmup(ctx); self.input_context.warmup(ctx); - self.parents.warmup(ctx); - - // Initialize the parents stack in the root UI. - // Every other UI elements will reference it and push or pop UIs to it. - if !self.parents.get().is_seq() { - self.parents.set(Seq::new().as_ref().into()); - } - - // Context works the same - if !self.instance.get().is_seq() { - self.instance.set(Seq::new().as_ref().into()); - } Ok(()) } fn cleanup(&mut self) -> Result<(), &str> { - self.parents.cleanup(); self.graphics_context.cleanup(); self.input_context.cleanup(); self.contents.cleanup(); self.queue.cleanup(); - self.instance.cleanup(); + self.host.cleanup()?; Ok(()) } fn activate(&mut self, context: &Context, input: &Var) -> Result { - let gui_ctx = if let Some(gui_ctx) = &self.context { - gui_ctx - } else { - return Err("No UI context"); - }; - if self.contents.is_empty() { return Ok(*input); } - let raw_input = unsafe { - let inputs = shardsc::gfx_getEguiWindowInputs( + let egui_input = unsafe { + &*(shardsc::gfx_getEguiWindowInputs( self.input_translator.as_mut_ptr() as *mut shardsc::gfx_EguiInputTranslator, self.graphics_context.get(), self.input_context.get(), 1.0, - ) as *const egui_gfx::egui_Input; - egui_gfx::translate_raw_input(&*inputs) + ) as *const egui_gfx::egui_Input) }; - match raw_input { - Err(_error) => { - shlog_debug!("Input translation error: {:?}", _error); - Err("Input translation error") - } - Ok(raw_input) => { - let draw_scale = raw_input.pixels_per_point.unwrap_or(1.0); - - let mut error: Option<&str> = None; - let egui_output = gui_ctx.run(raw_input, |ctx| { - error = (|| -> Result<(), &str> { - // Push empty parent UI in case this context is nested inside another UI - util::update_seq(&mut self.parents, |seq| { - seq.push(Var::default()); - })?; - - let mut _output = Var::default(); - let wire_state = - util::with_object_stack_var(&mut self.instance, ctx, &EGUI_CTX_TYPE, || { - Ok(self.contents.activate(context, input, &mut _output)) - })?; - - if wire_state == WireState::Error { - return Err("Failed to activate UI contents"); - } - - // Pop empty parent UI - util::update_seq(&mut self.parents, |seq| { - seq.pop(); - })?; - - Ok(()) - })() - .err(); - }); - - if let Some(e) = error { - return Err(e); - } - - #[cfg(not(any(target_arch = "wasm32", target_os = "ios")))] - if let Some(url) = &egui_output.platform_output.open_url { - webbrowser::open(&url.url).map_err(|e| { - shlog_error!("{}", e); - "Failed to open URL." - })?; - } - - let queue_var = self.queue.get(); - unsafe { - let queue = shardsc::gfx_getDrawQueueFromVar(queue_var); - self.renderer.render( - gui_ctx, - egui_output, - queue as *const egui_gfx::gfx_DrawQueuePtr, - draw_scale, - )?; - } - - Ok(*input) - } + self + .host + .activate(&egui_input, &(&self.contents).into(), context, input)?; + let egui_output = self.host.get_egui_output(); + + let queue_var = self.queue.get(); + unsafe { + let queue = shardsc::gfx_getDrawQueueFromVar(queue_var); + self + .renderer + .render_with_native_output(egui_output, queue as *const egui_gfx::gfx_DrawQueuePtr); } + + Ok(*input) } } diff --git a/rust/src/shards/gui/raw_context.rs b/rust/src/shards/gui/egui_host.rs similarity index 86% rename from rust/src/shards/gui/raw_context.rs rename to rust/src/shards/gui/egui_host.rs index 65ca4e856b..7b5fd27058 100644 --- a/rust/src/shards/gui/raw_context.rs +++ b/rust/src/shards/gui/egui_host.rs @@ -34,7 +34,7 @@ use crate::types::ANY_TYPES; use crate::types::SHARDS_OR_NONE_TYPES; use std::ffi::CStr; -struct UIContext { +pub struct EguiHost { context: Option, full_output: Option, instance: ParamVar, @@ -42,8 +42,8 @@ struct UIContext { exposed: Vec, } -impl UIContext { - pub fn default() -> Self { +impl Default for EguiHost { + fn default() -> Self { let mut instance = ParamVar::default(); instance.set_name(CONTEXTS_NAME); @@ -81,6 +81,12 @@ impl UIContext { exposed: exposed, } } +} + +impl EguiHost { + pub fn get_context(&self) -> &egui::Context { + self.context.as_ref().expect("No UI context") + } pub fn get_exposed_info(&self) -> &Vec { &self.exposed @@ -117,7 +123,7 @@ impl UIContext { contents: &Shards, context: &Context, input: &Var, - ) -> Result { + ) -> Result { let gui_ctx = if let Some(gui_ctx) = &self.context { gui_ctx } else { @@ -195,43 +201,43 @@ impl UIContext { mod native { use egui_gfx::{egui_FullOutput, egui_Input}; - use super::UIContext; + use super::EguiHost; use crate::{ shardsc::{self, SHVar, Shards}, types::{Context, Var}, }; #[no_mangle] - unsafe extern "C" fn egui_createContext() -> *mut UIContext { - Box::into_raw(Box::new(UIContext::default())) + unsafe extern "C" fn egui_createHost() -> *mut EguiHost { + Box::into_raw(Box::new(EguiHost::default())) } #[no_mangle] - unsafe extern "C" fn egui_destroyContext(ptr: *mut UIContext) { + unsafe extern "C" fn egui_destroyHost(ptr: *mut EguiHost) { drop(Box::from_raw(ptr)) } #[no_mangle] - unsafe extern "C" fn egui_getExposedTypeInfo( - ptr: *mut UIContext, + unsafe extern "C" fn egui_hostGetExposedTypeInfo( + ptr: *mut EguiHost, out_info: *mut shardsc::SHExposedTypesInfo, ) { (*out_info) = (&(*ptr).exposed).into(); } #[no_mangle] - unsafe extern "C" fn egui_warmup(ptr: *mut UIContext, ctx: &Context) { + unsafe extern "C" fn egui_hostWarmup(ptr: *mut EguiHost, ctx: &Context) { (*ptr).warmup(&ctx).unwrap(); } #[no_mangle] - unsafe extern "C" fn egui_cleanup(ptr: *mut UIContext) { + unsafe extern "C" fn egui_hostCleanup(ptr: *mut EguiHost) { (*ptr).cleanup().unwrap(); } #[no_mangle] - unsafe extern "C" fn egui_activate( - ptr: *mut UIContext, + unsafe extern "C" fn egui_hostActivate( + ptr: *mut EguiHost, egui_input: *const egui_Input, contents: &Shards, context: &Context, @@ -248,7 +254,11 @@ mod native { } #[no_mangle] - unsafe extern "C" fn egui_getOutput(ptr: *const UIContext) -> *const egui_FullOutput { - &(*ptr).full_output.as_ref().expect("Expected egui output").full_output + unsafe extern "C" fn egui_hostGetOutput(ptr: *const EguiHost) -> *const egui_FullOutput { + &(*ptr) + .full_output + .as_ref() + .expect("Expected egui output") + .full_output } } diff --git a/rust/src/shards/gui/mod.rs b/rust/src/shards/gui/mod.rs index a3eb863308..4c40852b85 100644 --- a/rust/src/shards/gui/mod.rs +++ b/rust/src/shards/gui/mod.rs @@ -91,15 +91,15 @@ impl From for egui::Id { } } +mod egui_host; + struct EguiContext { - context: Option, - instance: ParamVar, + host: egui_host::EguiHost, requiring: ExposedTypes, queue: ParamVar, contents: ShardsVar, graphics_context: ParamVar, input_context: ParamVar, - parents: ParamVar, renderer: egui_gfx::Renderer, input_translator: egui_gfx::InputTranslator, } @@ -112,7 +112,7 @@ mod misc; mod properties; mod util; mod widgets; -mod raw_context; + struct MutableVar<'a>(&'a mut Var); struct ImmutableVar<'a>(&'a Var); diff --git a/rust/src/types.rs b/rust/src/types.rs index d7aafd4952..d2ea492bae 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -3452,6 +3452,12 @@ impl ShardsVar { } } +impl From<&ShardsVar> for Shards { + fn from(v: &ShardsVar) -> Self { + v.native_shards + } +} + // Enum #[macro_export] @@ -3774,15 +3780,23 @@ impl Iterator for SeqIterator { } impl DoubleEndedIterator for SeqIterator { - fn next_back(&mut self) -> Option { - let res = if self.i < self.s.s.len { - unsafe { Some(*self.s.s.elements.offset((self.s.s.len - self.i - 1).try_into().unwrap())) } - } else { - None - }; - self.i += 1; - res - } + fn next_back(&mut self) -> Option { + let res = if self.i < self.s.s.len { + unsafe { + Some( + *self + .s + .s + .elements + .offset((self.s.s.len - self.i - 1).try_into().unwrap()), + ) + } + } else { + None + }; + self.i += 1; + res + } } impl IntoIterator for Seq { diff --git a/src/extra/egui/context.hpp b/src/extra/egui/context.hpp index 3b7028edde..b699e32cd2 100644 --- a/src/extra/egui/context.hpp +++ b/src/extra/egui/context.hpp @@ -4,17 +4,18 @@ #include #include -typedef void *EguiContext; +typedef void *EguiHost; extern "C" { -EguiContext egui_createContext(); -void egui_destroyContext(EguiContext); -void egui_getExposedTypeInfo(EguiContext, SHExposedTypesInfo &outTypes); -void egui_warmup(EguiContext, SHContext *shContext); -void egui_cleanup(EguiContext); -const char *egui_activate(EguiContext eguiContext, const egui::Input &eguiInput, const Shards &shards, SHContext *shContext, +EguiHost egui_createHost(); +void egui_destroyHost(EguiHost); + +void egui_hostGetExposedTypeInfo(EguiHost, SHExposedTypesInfo &outTypes); +void egui_hostWarmup(EguiHost, SHContext *shContext); +void egui_hostCleanup(EguiHost); +const char *egui_hostActivate(EguiHost, const egui::Input &eguiInput, const Shards &shards, SHContext *shContext, const SHVar &input, SHVar &output); -const egui::FullOutput *egui_getOutput(const EguiContext eguiContext); +const egui::FullOutput *egui_hostGetOutput(const EguiHost); } #endif /* ED3C732D_AC03_4B15_8EC8_BB15262E26DA */ diff --git a/src/extra/vui/vui.cpp b/src/extra/vui/vui.cpp index f01cf818cd..5f39596031 100644 --- a/src/extra/vui/vui.cpp +++ b/src/extra/vui/vui.cpp @@ -136,7 +136,7 @@ struct VUIPanelShard { RequiredVUIContext _context; Shard *_uiShard{}; - EguiContext _eguiContext; + EguiHost _eguiHost; ExposedInfo _exposedTypes; std::shared_ptr _panel; @@ -149,17 +149,17 @@ struct VUIPanelShard { return e; } - void ensureEguiContext() { - if (!_eguiContext) - _eguiContext = egui_createContext(); + void ensureEguiHost() { + if (!_eguiHost) + _eguiHost = egui_createHost(); } void warmup(SHContext *context) { PARAM_WARMUP(context); _context.warmup(context); - ensureEguiContext(); - egui_warmup(_eguiContext, context); + ensureEguiHost(); + egui_hostWarmup(_eguiHost, context); _panel = std::make_shared(*this); _context->context.panels.emplace_back(_panel); @@ -169,20 +169,20 @@ struct VUIPanelShard { PARAM_CLEANUP(); _context.cleanup(); - if (_eguiContext) { - egui_cleanup(_eguiContext); - egui_destroyContext(_eguiContext); - _eguiContext = nullptr; + if (_eguiHost) { + egui_hostCleanup(_eguiHost); + egui_destroyHost(_eguiHost); + _eguiHost = nullptr; } _panel.reset(); } SHTypeInfo compose(SHInstanceData &data) { - ensureEguiContext(); + ensureEguiHost(); SHExposedTypesInfo eguiExposedTypes{}; - egui_getExposedTypeInfo(_eguiContext, eguiExposedTypes); + egui_hostGetExposedTypeInfo(_eguiHost, eguiExposedTypes); _exposedTypes = ExposedInfo(data.shared); mergeIntoExposedInfo(_exposedTypes, eguiExposedTypes); @@ -201,11 +201,11 @@ struct VUIPanelShard { // This evaluates the egui contents for this panel virtual const egui::FullOutput &render(const egui::Input &inputs) { SHVar output{}; - const char *error = egui_activate(_eguiContext, inputs, _contents.shards(), _context->activationContext, SHVar{}, output); + const char *error = egui_hostActivate(_eguiHost, inputs, _contents.shards(), _context->activationContext, SHVar{}, output); if (error) throw ActivationError(fmt::format("egui activation error: {}", error)); - const egui::FullOutput &eguiOutput = *egui_getOutput(_eguiContext); + const egui::FullOutput &eguiOutput = *egui_hostGetOutput(_eguiHost); return eguiOutput; } diff --git a/src/gfx/egui/src/renderer.rs b/src/gfx/egui/src/renderer.rs index c7813a1ba8..094d363009 100644 --- a/src/gfx/egui/src/renderer.rs +++ b/src/gfx/egui/src/renderer.rs @@ -217,14 +217,18 @@ impl Renderer { queue: *const gfx_DrawQueuePtr, draw_scale: f32, ) -> Result<(), &str> { + let native_egui_output = make_native_full_output(ctx, egui_output, draw_scale)?; + self.render_with_native_output(&native_egui_output.full_output, queue); + Ok(()) + } + + pub fn render_with_native_output( + &self, + egui_output: *const egui_FullOutput, + queue: *const gfx_DrawQueuePtr, + ) { unsafe { - let native_egui_output = make_native_full_output(ctx, egui_output, draw_scale)?; - gfx_EguiRenderer_renderNoTransform( - self.egui_renderer, - &native_egui_output.full_output, - queue, - ); - Ok(()) + gfx_EguiRenderer_renderNoTransform(self.egui_renderer, egui_output, queue); } } } diff --git a/src/tests/vui.edn b/src/tests/vui.edn index 1499369d3a..b496003687 100644 --- a/src/tests/vui.edn +++ b/src/tests/vui.edn @@ -84,4 +84,4 @@ (GFX.Render :Steps .render-steps :View .view)))) (schedule Root test-wire) -(if (run Root timestep) nil (throw "Root tick failed")) +(if (run Root timestep 400) nil (throw "Root tick failed")) From 47a8405c53b07f8d8cba3bba8a364caa4f5b9263 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Fri, 20 Jan 2023 22:07:27 +0100 Subject: [PATCH 09/14] Cleanup --- docs/details/shards/VUI/Context.md | 1 - rust/src/shards/gui/egui_host.rs | 9 ++++----- src/extra/egui/context.cpp | 1 - src/extra/vui/vui.cpp | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 src/extra/egui/context.cpp diff --git a/docs/details/shards/VUI/Context.md b/docs/details/shards/VUI/Context.md index df44997153..109586716f 100644 --- a/docs/details/shards/VUI/Context.md +++ b/docs/details/shards/VUI/Context.md @@ -5,6 +5,5 @@ Every instance of [VUI.Panel](../Panel) should be placed within this shards's Co The draw commands for the UI elements are added to the Queue that is passed in. In contrast to the screen space [UI](../../General/UI) shard, they do not need to be rendered using a [GFX.UIPass](../../GFX/UIPass) !!! note - When rendering the draw commands, the sorting mode should be set to [SortMode.Queue](../../../enums/SortMode) so they are drawn in the correct order. The [BuiltinFeatureId.Transform](../../GFX/BuiltinFeature/#transform) feature should be used diff --git a/rust/src/shards/gui/egui_host.rs b/rust/src/shards/gui/egui_host.rs index 7b5fd27058..da2026167c 100644 --- a/rust/src/shards/gui/egui_host.rs +++ b/rust/src/shards/gui/egui_host.rs @@ -1,8 +1,3 @@ -use egui_gfx::egui_FullOutput; -use egui_gfx::egui_Input; -use egui_gfx::make_native_full_output; -use egui_gfx::NativeFullOutput; - use super::util; use super::EguiContext; use super::CONTEXTS_NAME; @@ -32,6 +27,10 @@ use crate::types::Var; use crate::types::WireState; use crate::types::ANY_TYPES; use crate::types::SHARDS_OR_NONE_TYPES; +use egui_gfx::egui_FullOutput; +use egui_gfx::egui_Input; +use egui_gfx::make_native_full_output; +use egui_gfx::NativeFullOutput; use std::ffi::CStr; pub struct EguiHost { diff --git a/src/extra/egui/context.cpp b/src/extra/egui/context.cpp deleted file mode 100644 index e8ad4b7e1f..0000000000 --- a/src/extra/egui/context.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "context.cpp" \ No newline at end of file diff --git a/src/extra/vui/vui.cpp b/src/extra/vui/vui.cpp index 5f39596031..69db852923 100644 --- a/src/extra/vui/vui.cpp +++ b/src/extra/vui/vui.cpp @@ -35,7 +35,7 @@ struct VUIContextShard { PARAM_PARAMVAR(_view, "View", "The view that is being used to render.", {Type::VariableOf(gfx::Types::View)}); PARAM(ShardsVar, _contents, "Contents", "The list of UI panels to render.", {CoreInfo::ShardsOrNone}); PARAM_VAR(_scale, "Scale", "The scale of how many UI units per world unit.", {CoreInfo::FloatType}); - PARAM_VAR(_debug, "Debug", "Render debug.", {CoreInfo::BoolType}); + PARAM_VAR(_debug, "Debug", "Visualize panel outlines and pointer input being sent to panels.", {CoreInfo::BoolType}); PARAM_IMPL(VUIContextShard, PARAM_IMPL_FOR(_queue), PARAM_IMPL_FOR(_view), PARAM_IMPL_FOR(_contents), PARAM_IMPL_FOR(_scale), PARAM_IMPL_FOR(_debug)); From f6b66704b1dded92379a33df93862ea281361cc2 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Fri, 20 Jan 2023 22:13:49 +0100 Subject: [PATCH 10/14] VUI passthrough --- src/extra/egui/pass.cpp | 2 +- src/extra/vui/vui.cpp | 13 +++++++------ src/extra/vui/vui.hpp | 1 + src/tests/vui.edn | 8 ++++++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/extra/egui/pass.cpp b/src/extra/egui/pass.cpp index 808b94e35d..6bd2bebc10 100644 --- a/src/extra/egui/pass.cpp +++ b/src/extra/egui/pass.cpp @@ -48,4 +48,4 @@ struct UIPassShard { }; void registerShards() { REGISTER_SHARD("GFX.UIPass", UIPassShard); } -} // namespace shards::egui \ No newline at end of file +} // namespace shards::egui diff --git a/src/extra/vui/vui.cpp b/src/extra/vui/vui.cpp index 69db852923..5af4430fdf 100644 --- a/src/extra/vui/vui.cpp +++ b/src/extra/vui/vui.cpp @@ -44,8 +44,9 @@ struct VUIContextShard { _debug = Var(false); } - static SHTypesInfo inputTypes() { return CoreInfo::NoneType; } - static SHTypesInfo outputTypes() { return CoreInfo::NoneType; } + static SHTypesInfo inputTypes() { return CoreInfo::AnyType; } + static SHTypesInfo outputTypes() { return CoreInfo::AnyType; } + static SHOptionalString help() { return SHCCSTR("Creates a context for virtual UI panels to make sure input is correctly handled between them"); } @@ -86,7 +87,7 @@ struct VUIContextShard { if (_view->valueType == SHType::None) throw ComposeError("View is required"); - return shards::CoreInfo::NoneType; + return data.inputType; } SHVar activate(SHContext *shContext, const SHVar &input) { @@ -103,6 +104,7 @@ struct VUIContextShard { // Evaluate all UI panels _vuiContext.activationContext = shContext; + _vuiContext.activationInput = input; _vuiContext.context.virtualPointScale = _scale.payload.floatValue; withObjectVariable(*_vuiContextVar, &_vuiContext, VUIContext::Type, [&]() { gfx::SizedView sizedView(view.view, gfx::float2(viewStackTop.viewport.getSize())); @@ -122,8 +124,7 @@ struct VUIContextShard { _debugRenderer->end(queue.queue); } - SHVar output{}; - return output; + return input; } }; @@ -201,7 +202,7 @@ struct VUIPanelShard { // This evaluates the egui contents for this panel virtual const egui::FullOutput &render(const egui::Input &inputs) { SHVar output{}; - const char *error = egui_hostActivate(_eguiHost, inputs, _contents.shards(), _context->activationContext, SHVar{}, output); + const char *error = egui_hostActivate(_eguiHost, inputs, _contents.shards(), _context->activationContext, _context->activationInput, output); if (error) throw ActivationError(fmt::format("egui activation error: {}", error)); diff --git a/src/extra/vui/vui.hpp b/src/extra/vui/vui.hpp index 788c95b9fd..50f156fd37 100644 --- a/src/extra/vui/vui.hpp +++ b/src/extra/vui/vui.hpp @@ -26,6 +26,7 @@ struct VUIContext { static inline SHExposedTypeInfo VariableInfo = shards::ExposedInfo::ProtectedVariable(VariableName, VariableDescription, Type); SHContext *activationContext{}; + SHVar activationInput{}; vui::Context context; std::vector panels; }; diff --git a/src/tests/vui.edn b/src/tests/vui.edn index b496003687..c2be35867a 100644 --- a/src/tests/vui.edn +++ b/src/tests/vui.edn @@ -38,7 +38,8 @@ .view-transform (FreeCamera :FlySpeed 10.0) > .view-transform .queue (GFX.ClearQueue) - (VUI.Context + + 20.23 (VUI.Context :Queue .queue :View .view :Scale 100.0 :Contents (-> (VUI.Panel @@ -46,6 +47,7 @@ (-> (UI.CentralPanel (-> + (ToString) (UI.Label) "First panel" (UI.Label) (UI.Button :Label "Button"))))) (VUI.Panel @@ -53,6 +55,7 @@ (-> (UI.CentralPanel (-> + (ToString) (UI.Label) "Some other panel" (UI.Label) (UI.Button :Label "Button"))))) (VUI.Panel @@ -60,6 +63,7 @@ (-> (UI.CentralPanel (-> + (ToString) (UI.Label) "Wide panel" (UI.Label) (Setup (LoadImage "../../assets/ShardsLogo.png") (GFX.Texture) >= .button-texture) @@ -84,4 +88,4 @@ (GFX.Render :Steps .render-steps :View .view)))) (schedule Root test-wire) -(if (run Root timestep 400) nil (throw "Root tick failed")) +(if (run Root timestep 100) nil (throw "Root tick failed")) From ec17105fbef09c726d0ecc88faee1d88b27c6f52 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Fri, 20 Jan 2023 22:16:56 +0100 Subject: [PATCH 11/14] Any input type for VUI.Panel --- src/extra/vui/vui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extra/vui/vui.cpp b/src/extra/vui/vui.cpp index 5af4430fdf..391e1ecf1e 100644 --- a/src/extra/vui/vui.cpp +++ b/src/extra/vui/vui.cpp @@ -141,7 +141,7 @@ struct VUIPanelShard { ExposedInfo _exposedTypes; std::shared_ptr _panel; - static SHTypesInfo inputTypes() { return CoreInfo::NoneType; } + static SHTypesInfo inputTypes() { return CoreInfo::AnyType; } static SHTypesInfo outputTypes() { return CoreInfo::NoneType; } static SHOptionalString help() { return SHCCSTR("Defines a virtual UI panel"); } From 5a85265744da402033cba454723b1c8dcd141b99 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Tue, 24 Jan 2023 19:15:18 +0100 Subject: [PATCH 12/14] Rename VUI to Spatial Also Rename Context => UI --- cmake/Root.cmake | 2 +- docs/details/shards/{VUI => Spatial}/Panel.md | 6 +- .../shards/{VUI/Context.md => Spatial/UI.md} | 2 +- .../{VUI => Spatial}/assets/render_output.png | Bin .../shards/{VUI/Context => Spatial/UI}/1.edn | 8 +- .../{VUI/Context => Spatial/UI}/1.out.md | 0 src/extra/CMakeLists.txt | 4 +- src/extra/runtime.cpp | 4 +- .../{vui/vui.cpp => spatial/spatial.cpp} | 78 +++++++++--------- .../{vui/vui.hpp => spatial/spatial.hpp} | 28 +++---- src/spatial/CMakeLists.txt | 4 + src/{virtual_ui => spatial}/context.cpp | 4 +- src/{virtual_ui => spatial}/context.hpp | 6 +- src/spatial/panel.cpp | 4 + src/{virtual_ui => spatial}/panel.hpp | 4 +- src/spatial/readme.md | 3 + src/tests/{vui.edn => spatial.edn} | 8 +- src/virtual_ui/CMakeLists.txt | 4 - src/virtual_ui/panel.cpp | 4 - 19 files changed, 87 insertions(+), 86 deletions(-) rename docs/details/shards/{VUI => Spatial}/Panel.md (65%) rename docs/details/shards/{VUI/Context.md => Spatial/UI.md} (84%) rename docs/docs/reference/shards/{VUI => Spatial}/assets/render_output.png (100%) rename docs/samples/shards/{VUI/Context => Spatial/UI}/1.edn (96%) rename docs/samples/shards/{VUI/Context => Spatial/UI}/1.out.md (100%) rename src/extra/{vui/vui.cpp => spatial/spatial.cpp} (71%) rename src/extra/{vui/vui.hpp => spatial/spatial.hpp} (52%) create mode 100644 src/spatial/CMakeLists.txt rename src/{virtual_ui => spatial}/context.cpp (99%) rename src/{virtual_ui => spatial}/context.hpp (95%) create mode 100644 src/spatial/panel.cpp rename src/{virtual_ui => spatial}/panel.hpp (94%) create mode 100644 src/spatial/readme.md rename src/tests/{vui.edn => spatial.edn} (97%) delete mode 100644 src/virtual_ui/CMakeLists.txt delete mode 100644 src/virtual_ui/panel.cpp diff --git a/cmake/Root.cmake b/cmake/Root.cmake index bb5309d244..d1039db29d 100644 --- a/cmake/Root.cmake +++ b/cmake/Root.cmake @@ -32,7 +32,7 @@ add_subdirectory(${SHARDS_DIR}/src/core src/core) add_subdirectory(${SHARDS_DIR}/src/mal src/mal) add_subdirectory(${SHARDS_DIR}/src/input src/input) add_subdirectory(${SHARDS_DIR}/src/gfx src/gfx) -add_subdirectory(${SHARDS_DIR}/src/virtual_ui src/virtual_ui) +add_subdirectory(${SHARDS_DIR}/src/spatial src/spatial) if(SHARDS_WITH_EXTRA_SHARDS) if(SHARDS_WITH_RUST_SHARDS) diff --git a/docs/details/shards/VUI/Panel.md b/docs/details/shards/Spatial/Panel.md similarity index 65% rename from docs/details/shards/VUI/Panel.md rename to docs/details/shards/Spatial/Panel.md index 8361f2a6a5..4f44da83a4 100644 --- a/docs/details/shards/VUI/Panel.md +++ b/docs/details/shards/Spatial/Panel.md @@ -1,10 +1,10 @@ -This shards defines a single world space UI panel. This shard needs to be placed within a [VUI.Context](../Context)'s Contents block. +This shards defines a single world space UI panel. This shard needs to be placed within a [Spatial.UI](../UI)'s Contents block. -This size of the panels defined using virtual UI points/units/pixels. How these virtual points map to world coordinates is controlled by the Context's Scale parameter. For example if the Context's scale is set to 200; 200 virtual points will be equal to 1 unit in world space. +This size of the panels defined using virtual UI points/units/pixels. How these virtual points map to world coordinates is controlled by the UI's Scale parameter. For example if the UI's scale is set to 200; 200 virtual points will be equal to 1 unit in world space. A panel's position and orientation in the world is controlled by the `Transform` parameter, which is a standard 4x4 transformation matrix. !!! danger Applying scaling on this transform is not recommended and may result in undefined behavior. -The Panels' contents should be placed in the `Contents` parameter, similar to the screen space [UI](../../General/UI) \ No newline at end of file +The Panels' contents should be placed in the `Contents` parameter, similar to the screen space [UI](../../General/UI) diff --git a/docs/details/shards/VUI/Context.md b/docs/details/shards/Spatial/UI.md similarity index 84% rename from docs/details/shards/VUI/Context.md rename to docs/details/shards/Spatial/UI.md index 109586716f..49e27ce738 100644 --- a/docs/details/shards/VUI/Context.md +++ b/docs/details/shards/Spatial/UI.md @@ -1,6 +1,6 @@ This shard defines the starting point for creating interactive world space UI elements. -Every instance of [VUI.Panel](../Panel) should be placed within this shards's Contents parameter. +Every instance of [Spatial.Panel](../Panel) should be placed within this shards's Contents parameter. The draw commands for the UI elements are added to the Queue that is passed in. In contrast to the screen space [UI](../../General/UI) shard, they do not need to be rendered using a [GFX.UIPass](../../GFX/UIPass) diff --git a/docs/docs/reference/shards/VUI/assets/render_output.png b/docs/docs/reference/shards/Spatial/assets/render_output.png similarity index 100% rename from docs/docs/reference/shards/VUI/assets/render_output.png rename to docs/docs/reference/shards/Spatial/assets/render_output.png diff --git a/docs/samples/shards/VUI/Context/1.edn b/docs/samples/shards/Spatial/UI/1.edn similarity index 96% rename from docs/samples/shards/VUI/Context/1.edn rename to docs/samples/shards/Spatial/UI/1.edn index 18d1c9200f..1778317087 100644 --- a/docs/samples/shards/VUI/Context/1.edn +++ b/docs/samples/shards/Spatial/UI/1.edn @@ -22,24 +22,24 @@ :Contents (-> .queue (GFX.ClearQueue) - (VUI.Context + (Spatial.UI :Queue .queue :View .view :Scale 100.0 :Contents (-> - (VUI.Panel + (Spatial.Panel :Transform .panel-t-0 :Size (Float2 100 100) :Contents (-> (UI.CentralPanel (-> "Left panel" (UI.Label) (UI.Button :Label "Button"))))) - (VUI.Panel + (Spatial.Panel :Transform .panel-t-1 :Size (Float2 100 100) :Contents (-> (UI.CentralPanel (-> "Right panel" (UI.Label) (UI.Button :Label "Button"))))) - (VUI.Panel + (Spatial.Panel :Transform .panel-t-2 :Size (Float2 300 60) :Contents (-> (UI.CentralPanel diff --git a/docs/samples/shards/VUI/Context/1.out.md b/docs/samples/shards/Spatial/UI/1.out.md similarity index 100% rename from docs/samples/shards/VUI/Context/1.out.md rename to docs/samples/shards/Spatial/UI/1.out.md diff --git a/src/extra/CMakeLists.txt b/src/extra/CMakeLists.txt index 19cef97b19..2ff7077823 100644 --- a/src/extra/CMakeLists.txt +++ b/src/extra/CMakeLists.txt @@ -19,7 +19,7 @@ set(extra_SOURCES gfx/texture.cpp gfx/view.cpp gfx/steps.cpp - vui/vui.cpp + spatial/spatial.cpp egui/pass.cpp rust_interop.cpp gizmos/context.cpp @@ -44,7 +44,7 @@ target_link_libraries(shards-extra SDL2) target_link_libraries(shards-extra shards-core - stb gfx gfx-gltf gfx-egui input virtual-ui + stb gfx gfx-gltf gfx-egui input spatial brotlienc-static brotlidec-static brotlicommon-static snappy kissfft miniaudio nlohmann_json diff --git a/src/extra/runtime.cpp b/src/extra/runtime.cpp index 5f8a134b31..c357c74c92 100644 --- a/src/extra/runtime.cpp +++ b/src/extra/runtime.cpp @@ -50,7 +50,7 @@ namespace egui { extern void registerShards(); } -namespace VUI { +namespace Spatial { extern void registerShards(); } @@ -68,7 +68,7 @@ void shInitExtras() { Audio::registerShards(); DSP::registerShards(); egui::registerShards(); - VUI::registerShards(); + Spatial::registerShards(); #ifdef _WIN32 Desktop::registerDesktopShards(); diff --git a/src/extra/vui/vui.cpp b/src/extra/spatial/spatial.cpp similarity index 71% rename from src/extra/vui/vui.cpp rename to src/extra/spatial/spatial.cpp index 391e1ecf1e..3708920b05 100644 --- a/src/extra/vui/vui.cpp +++ b/src/extra/spatial/spatial.cpp @@ -1,4 +1,4 @@ -#include "vui.hpp" +#include "spatial.hpp" #include "../gfx.hpp" #include "../inputs.hpp" #include "gfx/egui/egui_types.hpp" @@ -11,21 +11,19 @@ #include #include -namespace shards::VUI { +namespace shards::Spatial { -#define VUI_PANEL_SHARD_NAME "VUI.Panel" - -struct VUIPanelShard; -struct VUIContextShard { - VUIContext _vuiContext{}; - SHVar *_vuiContextVar{}; +struct SpatialPanelShard; +struct SpatialUIContextShard { + SpatialContext _spatialContext{}; + SHVar *_spatialContextVar{}; Inputs::RequiredInputContext _inputContext; gfx::RequiredGraphicsContext _graphicsContext; ExposedInfo _exposedVariables; - std::vector _panels; + std::vector _panels; input::InputBuffer _inputBuffer; @@ -36,10 +34,10 @@ struct VUIContextShard { PARAM(ShardsVar, _contents, "Contents", "The list of UI panels to render.", {CoreInfo::ShardsOrNone}); PARAM_VAR(_scale, "Scale", "The scale of how many UI units per world unit.", {CoreInfo::FloatType}); PARAM_VAR(_debug, "Debug", "Visualize panel outlines and pointer input being sent to panels.", {CoreInfo::BoolType}); - PARAM_IMPL(VUIContextShard, PARAM_IMPL_FOR(_queue), PARAM_IMPL_FOR(_view), PARAM_IMPL_FOR(_contents), PARAM_IMPL_FOR(_scale), + PARAM_IMPL(SpatialUIContextShard, PARAM_IMPL_FOR(_queue), PARAM_IMPL_FOR(_view), PARAM_IMPL_FOR(_contents), PARAM_IMPL_FOR(_scale), PARAM_IMPL_FOR(_debug)); - VUIContextShard() { + SpatialUIContextShard() { _scale = Var(1000.0f); _debug = Var(false); } @@ -48,7 +46,7 @@ struct VUIContextShard { static SHTypesInfo outputTypes() { return CoreInfo::AnyType; } static SHOptionalString help() { - return SHCCSTR("Creates a context for virtual UI panels to make sure input is correctly handled between them"); + return SHCCSTR("Creates a context for spatial UI panels to make sure input is correctly handled between them"); } SHExposedTypesInfo requiredVariables() { @@ -66,17 +64,17 @@ struct VUIContextShard { _graphicsContext.cleanup(); _inputContext.cleanup(); - if (_vuiContextVar) { - if (_vuiContextVar->refcount > 1) { - SHLOG_ERROR("VUI.Context: Found {} dangling reference(s) to {}", _vuiContextVar->refcount - 1, VUIContext::VariableName); + if (_spatialContextVar) { + if (_spatialContextVar->refcount > 1) { + SHLOG_ERROR("Spatial.UI: Found {} dangling reference(s) to {}", _spatialContextVar->refcount - 1, SpatialContext::VariableName); } - releaseVariable(_vuiContextVar); + releaseVariable(_spatialContextVar); } } SHTypeInfo compose(SHInstanceData &data) { _exposedVariables = ExposedInfo(data.shared); - _exposedVariables.push_back(VUIContext::VariableInfo); + _exposedVariables.push_back(SpatialContext::VariableInfo); data.shared = SHExposedTypesInfo(_exposedVariables); _contents.compose(data); @@ -103,15 +101,15 @@ struct VUIContextShard { _inputBuffer.push_back(event); // Evaluate all UI panels - _vuiContext.activationContext = shContext; - _vuiContext.activationInput = input; - _vuiContext.context.virtualPointScale = _scale.payload.floatValue; - withObjectVariable(*_vuiContextVar, &_vuiContext, VUIContext::Type, [&]() { + _spatialContext.activationContext = shContext; + _spatialContext.activationInput = input; + _spatialContext.context.virtualPointScale = _scale.payload.floatValue; + withObjectVariable(*_spatialContextVar, &_spatialContext, SpatialContext::Type, [&]() { gfx::SizedView sizedView(view.view, gfx::float2(viewStackTop.viewport.getSize())); - _vuiContext.context.prepareInputs(_inputBuffer, _graphicsContext->window->getInputScale(), sizedView); - _vuiContext.context.evaluate(queue.queue, _graphicsContext->time, _graphicsContext->deltaTime); + _spatialContext.context.prepareInputs(_inputBuffer, _graphicsContext->window->getInputScale(), sizedView); + _spatialContext.context.evaluate(queue.queue, _graphicsContext->time, _graphicsContext->deltaTime); }); - _vuiContext.activationContext = nullptr; + _spatialContext.activationContext = nullptr; // Render debug overlay if ((bool)_debug) { @@ -120,7 +118,7 @@ struct VUIContextShard { } _debugRenderer->begin(view.view, gfx::float2(viewStackTop.viewport.getSize())); - _vuiContext.context.renderDebug(_debugRenderer->getShapeRenderer()); + _spatialContext.context.renderDebug(_debugRenderer->getShapeRenderer()); _debugRenderer->end(queue.queue); } @@ -128,14 +126,14 @@ struct VUIContextShard { } }; -struct VUIPanelShard { +struct SpatialPanelShard { PARAM_PARAMVAR(_transform, "Transform", "The world transform of this panel.", {CoreInfo::Float4x4Type, Type::VariableOf(CoreInfo::Float4x4Type)}); PARAM_PARAMVAR(_size, "Size", "The size of the panel.", {CoreInfo::Float2Type, Type::VariableOf(CoreInfo::Float2Type)}); PARAM(ShardsVar, _contents, "Contents", "The panel UI contents.", {CoreInfo::ShardsOrNone}); - PARAM_IMPL(VUIPanelShard, PARAM_IMPL_FOR(_transform), PARAM_IMPL_FOR(_size), PARAM_IMPL_FOR(_contents)); + PARAM_IMPL(SpatialPanelShard, PARAM_IMPL_FOR(_transform), PARAM_IMPL_FOR(_size), PARAM_IMPL_FOR(_contents)); - RequiredVUIContext _context; + RequiredSpatialContext _context; Shard *_uiShard{}; EguiHost _eguiHost; ExposedInfo _exposedTypes; @@ -143,10 +141,10 @@ struct VUIPanelShard { static SHTypesInfo inputTypes() { return CoreInfo::AnyType; } static SHTypesInfo outputTypes() { return CoreInfo::NoneType; } - static SHOptionalString help() { return SHCCSTR("Defines a virtual UI panel"); } + static SHOptionalString help() { return SHCCSTR("Defines a spatial UI panel"); } SHExposedTypesInfo requiredVariables() { - static auto e = exposedTypesOf(RequiredVUIContext::getExposedTypeInfo()); + static auto e = exposedTypesOf(RequiredSpatialContext::getExposedTypeInfo()); return e; } @@ -196,7 +194,7 @@ struct VUIPanelShard { } SHVar activate(SHContext *shContext, const SHVar &input) { - throw ActivationError("Invalid activation, VUIPanel can not be used directly"); + throw ActivationError("Invalid activation, SpatialPanel can not be used directly"); } // This evaluates the egui contents for this panel @@ -210,11 +208,11 @@ struct VUIPanelShard { return eguiOutput; } - vui::PanelGeometry getGeometry() const { + spatial::PanelGeometry getGeometry() const { gfx::float4x4 transform = toFloat4x4(_transform.get()); gfx::float2 alignment{0.5f}; - vui::PanelGeometry result; + spatial::PanelGeometry result; result.anchor = gfx::extractTranslation(transform); result.up = transform.y.xyz(); result.right = transform.x.xyz(); @@ -226,19 +224,19 @@ struct VUIPanelShard { }; const egui::FullOutput &Panel::render(const egui::Input &inputs) { return panelShard.render(inputs); } -vui::PanelGeometry Panel::getGeometry() const { return panelShard.getGeometry(); } +spatial::PanelGeometry Panel::getGeometry() const { return panelShard.getGeometry(); } -void VUIContextShard::warmup(SHContext *context) { - _vuiContextVar = referenceVariable(context, VUIContext::VariableName); +void SpatialUIContextShard::warmup(SHContext *context) { + _spatialContextVar = referenceVariable(context, SpatialContext::VariableName); _inputContext.warmup(context); _graphicsContext.warmup(context); - withObjectVariable(*_vuiContextVar, &_vuiContext, VUIContext::Type, [&]() { PARAM_WARMUP(context); }); + withObjectVariable(*_spatialContextVar, &_spatialContext, SpatialContext::Type, [&]() { PARAM_WARMUP(context); }); } void registerShards() { - REGISTER_SHARD("VUI.Context", VUIContextShard); - REGISTER_SHARD(VUI_PANEL_SHARD_NAME, VUIPanelShard); + REGISTER_SHARD("Spatial.UI", SpatialUIContextShard); + REGISTER_SHARD("Spatial.Panel", SpatialPanelShard); } -} // namespace shards::VUI +} // namespace shards::Spatial diff --git a/src/extra/vui/vui.hpp b/src/extra/spatial/spatial.hpp similarity index 52% rename from src/extra/vui/vui.hpp rename to src/extra/spatial/spatial.hpp index 50f156fd37..b61c9d89f9 100644 --- a/src/extra/vui/vui.hpp +++ b/src/extra/spatial/spatial.hpp @@ -5,34 +5,34 @@ #include #include #include -#include -#include +#include +#include -namespace shards::VUI { +namespace shards::Spatial { -struct Panel : public shards::vui::Panel { - struct VUIPanelShard &panelShard; +struct Panel : public shards::spatial::Panel { + struct SpatialPanelShard &panelShard; - Panel(VUIPanelShard &panelShard) : panelShard(panelShard) {} + Panel(SpatialPanelShard &panelShard) : panelShard(panelShard) {} virtual const egui::FullOutput &render(const egui::Input &inputs); - virtual vui::PanelGeometry getGeometry() const; + virtual spatial::PanelGeometry getGeometry() const; }; -struct VUIContext { - static constexpr uint32_t TypeId = 'vuic'; +struct SpatialContext { + static constexpr uint32_t TypeId = 'spac'; static inline SHTypeInfo Type{SHType::Object, {.object = {.vendorId = CoreCC, .typeId = TypeId}}}; - static inline const char VariableName[] = "VUI.Context"; - static inline const SHOptionalString VariableDescription = SHCCSTR("The virtual UI context."); + static inline const char VariableName[] = "Spatial.UI"; + static inline const SHOptionalString VariableDescription = SHCCSTR("The spatial UI context."); static inline SHExposedTypeInfo VariableInfo = shards::ExposedInfo::ProtectedVariable(VariableName, VariableDescription, Type); SHContext *activationContext{}; SHVar activationInput{}; - vui::Context context; + spatial::Context context; std::vector panels; }; -typedef shards::RequiredContextVariable RequiredVUIContext; +typedef shards::RequiredContextVariable RequiredSpatialContext; -} // namespace shards::VUI +} // namespace shards::Spatial #endif /* A70CCFB2_9913_4743_9046_5624A7B1ED9A */ diff --git a/src/spatial/CMakeLists.txt b/src/spatial/CMakeLists.txt new file mode 100644 index 0000000000..9edb85b930 --- /dev/null +++ b/src/spatial/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(spatial context.cpp panel.cpp) +target_include_directories(spatial PUBLIC ${CMAKE_CURRENT_LIST_DIR}/..) +target_link_libraries(spatial SDL2 gfx) +target_compile_features(spatial PUBLIC cxx_std_20) diff --git a/src/virtual_ui/context.cpp b/src/spatial/context.cpp similarity index 99% rename from src/virtual_ui/context.cpp rename to src/spatial/context.cpp index 5dc8281d62..bc91219ef7 100644 --- a/src/virtual_ui/context.cpp +++ b/src/spatial/context.cpp @@ -8,7 +8,7 @@ #include using namespace gfx; -namespace shards::vui { +namespace shards::spatial { // Associated panel data struct ContextCachedPanel { @@ -270,4 +270,4 @@ std::shared_ptr Context::getCachedPanel(PanelPtr panel) { return it->second; } -} // namespace shards::vui +} // namespace shards::spatial diff --git a/src/virtual_ui/context.hpp b/src/spatial/context.hpp similarity index 95% rename from src/virtual_ui/context.hpp rename to src/spatial/context.hpp index 2643a63b93..05400385a0 100644 --- a/src/virtual_ui/context.hpp +++ b/src/spatial/context.hpp @@ -17,7 +17,7 @@ namespace gfx { struct ShapeRenderer; } -namespace shards::vui { +namespace shards::spatial { typedef std::shared_ptr PanelPtr; struct ContextCachedPanel; struct Context { @@ -35,7 +35,7 @@ struct Context { // Points per world space unit float virtualPointScale = 200.0f; - // Resolution of rendered UI, in actual pixels per virtual UI pixel + // Resolution of rendered UI, in actual pixels per spatial UI pixel float pixelsPerPoint = 2.0f; // Minimum resolution to render UI at from a distance @@ -80,6 +80,6 @@ struct Context { // Computes the render scale to render the given panel at, taking it's projected screen area into account float computeRenderResolution(PanelPtr panel) const; }; -} // namespace shards::vui +} // namespace shards::spatial #endif /* AA53E1AF_30CD_452D_8285_B9C96CC68AD6 */ diff --git a/src/spatial/panel.cpp b/src/spatial/panel.cpp new file mode 100644 index 0000000000..9580debb6b --- /dev/null +++ b/src/spatial/panel.cpp @@ -0,0 +1,4 @@ +#include "panel.hpp" +using namespace gfx; +namespace shards::spatial { +} // namespace shards::spatial diff --git a/src/virtual_ui/panel.hpp b/src/spatial/panel.hpp similarity index 94% rename from src/virtual_ui/panel.hpp rename to src/spatial/panel.hpp index 5bbb58299a..88b3616ccf 100644 --- a/src/virtual_ui/panel.hpp +++ b/src/spatial/panel.hpp @@ -5,7 +5,7 @@ #include #include -namespace shards::vui { +namespace shards::spatial { struct PanelGeometry { gfx::float3 anchor; gfx::float3 center; @@ -39,6 +39,6 @@ struct Panel { virtual const egui::FullOutput &render(const egui::Input &inputs) = 0; virtual PanelGeometry getGeometry() const = 0; }; -} // namespace shards::vui +} // namespace shards::spatial #endif /* CA03D35E_D054_4C56_9F4B_949787F0B26F */ diff --git a/src/spatial/readme.md b/src/spatial/readme.md new file mode 100644 index 0000000000..f05026f91b --- /dev/null +++ b/src/spatial/readme.md @@ -0,0 +1,3 @@ +# Spatial + +This folder contains code for dealing with world space UI and related tasks diff --git a/src/tests/vui.edn b/src/tests/spatial.edn similarity index 97% rename from src/tests/vui.edn rename to src/tests/spatial.edn index c2be35867a..c8fe5517d5 100644 --- a/src/tests/vui.edn +++ b/src/tests/spatial.edn @@ -39,10 +39,10 @@ .queue (GFX.ClearQueue) - 20.23 (VUI.Context + 20.23 (Spatial.Context :Queue .queue :View .view :Scale 100.0 :Contents (-> - (VUI.Panel + (Spatial.Panel :Transform .panel-t-0 :Size (Float2 100 100) :Contents (-> (UI.CentralPanel @@ -50,7 +50,7 @@ (ToString) (UI.Label) "First panel" (UI.Label) (UI.Button :Label "Button"))))) - (VUI.Panel + (Spatial.Panel :Transform .panel-t-1 :Size (Float2 100 100) :Contents (-> (UI.CentralPanel @@ -58,7 +58,7 @@ (ToString) (UI.Label) "Some other panel" (UI.Label) (UI.Button :Label "Button"))))) - (VUI.Panel + (Spatial.Panel :Transform .panel-t-2 :Size (Float2 300 60) :Contents (-> (UI.CentralPanel diff --git a/src/virtual_ui/CMakeLists.txt b/src/virtual_ui/CMakeLists.txt deleted file mode 100644 index ba84b6ea89..0000000000 --- a/src/virtual_ui/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_library(virtual-ui context.cpp panel.cpp) -target_include_directories(virtual-ui PUBLIC ${CMAKE_CURRENT_LIST_DIR}/..) -target_link_libraries(virtual-ui SDL2 gfx) -target_compile_features(virtual-ui PUBLIC cxx_std_20) diff --git a/src/virtual_ui/panel.cpp b/src/virtual_ui/panel.cpp deleted file mode 100644 index 0013ac6631..0000000000 --- a/src/virtual_ui/panel.cpp +++ /dev/null @@ -1,4 +0,0 @@ -#include "panel.hpp" -using namespace gfx; -namespace shards::vui { -} // namespace shards::vui From a5f17b3295af1fed5325038d98b63c32a61200c1 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Fri, 27 Jan 2023 17:14:12 +0800 Subject: [PATCH 13/14] Fix test --- src/tests/spatial.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/spatial.edn b/src/tests/spatial.edn index c8fe5517d5..7824be3214 100644 --- a/src/tests/spatial.edn +++ b/src/tests/spatial.edn @@ -39,7 +39,7 @@ .queue (GFX.ClearQueue) - 20.23 (Spatial.Context + 20.23 (Spatial.UI :Queue .queue :View .view :Scale 100.0 :Contents (-> (Spatial.Panel From 27c19ed981bfa04a980ce85a8ff87a160ca73423 Mon Sep 17 00:00:00 2001 From: Guus Waals <_@guusw.nl> Date: Fri, 27 Jan 2023 17:14:20 +0800 Subject: [PATCH 14/14] Fix panel focus when leaving cursor --- src/spatial/context.cpp | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/spatial/context.cpp b/src/spatial/context.cpp index bc91219ef7..458e5b0736 100644 --- a/src/spatial/context.cpp +++ b/src/spatial/context.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace gfx; namespace shards::spatial { @@ -48,6 +49,12 @@ void Context::prepareInputs(input::InputBuffer &input, gfx::float2 inputToViewSc float uiToWorldScale = 1.0f / virtualPointScale; lastFocusedPanel = focusedPanel; + + // Reset focused panel when the cursor is moved + bool haveAnyPointerEvents = pointerInputs.size() > 0; + if (haveAnyPointerEvents) + focusedPanel.reset(); + for (auto &evt : pointerInputs) { for (size_t panelIndex = 0; panelIndex < panels.size(); panelIndex++) { auto &panel = panels[panelIndex]; @@ -139,16 +146,18 @@ void Context::evaluate(gfx::DrawQueuePtr queue, double time, float deltaTime) { pointerGone.type = egui::InputEventType::PointerGone; eguiInputTranslator.pushEvent(evt); - // Simulate deselect + // Simulate deselect by simulating up/down for all buttons auto &pointerButton = evt.pointerButton; - pointerButton.type = egui::InputEventType::PointerButton; - pointerButton.button = egui::PointerButton::Primary; - pointerButton.pos = egui::toPos2(float2(-1, -1)); - pointerButton.type = egui::InputEventType::PointerButton; - pointerButton.pressed = true; - eguiInputTranslator.pushEvent(evt); - pointerButton.pressed = false; - eguiInputTranslator.pushEvent(evt); + for (auto &button : magic_enum::enum_values()) { + pointerButton.type = egui::InputEventType::PointerButton; + pointerButton.button = button; + pointerButton.pos = egui::toPos2(float2(-1, -1)); + pointerButton.type = egui::InputEventType::PointerButton; + pointerButton.pressed = true; + eguiInputTranslator.pushEvent(evt); + pointerButton.pressed = false; + eguiInputTranslator.pushEvent(evt); + } } for (auto &pointerInput : pointerInputs) {