From 4278fea692c314c6b1d0d0d70423d890353fd560 Mon Sep 17 00:00:00 2001 From: cheater Date: Mon, 19 Oct 2026 16:40:32 +0300 Subject: [PATCH 1/8] wip --- sources/HAL/autogen/autogen.cpp | 8 + sources/HAL/autogen/autogen.ixx | 6 + sources/HAL/autogen/enums.ixx | 4 + sources/HAL/autogen/pso.cpp | 3 + .../HAL/autogen/pso/UI/BackdropBlur.pso.ixx | 60 +++ .../HAL/autogen/pso/UI/BackdropDraw.pso.ixx | 65 +++ sources/HAL/autogen/slots/UI/BackdropBlur.ixx | 26 ++ sources/HAL/autogen/slots/UI/BackdropDraw.ixx | 26 ++ .../autogen/tables/UI/BackdropBlur.table.ixx | 78 ++++ .../autogen/tables/UI/BackdropDraw.table.ixx | 86 ++++ sources/Prism/defs/ui.prism | 56 +++ sources/RenderSystem/GUI/Base.cpp | 55 ++- sources/RenderSystem/GUI/Base.ixx | 42 ++ sources/RenderSystem/GUI/Elements/Button.cpp | 9 +- .../RenderSystem/GUI/Elements/ComboBox.cpp | 9 +- .../RenderSystem/GUI/Elements/Container.cpp | 4 + .../GUI/Elements/FlowGraph/Canvas.cpp | 10 +- .../Elements/FlowGraph/ComponentWindow.cpp | 1 + .../RenderSystem/GUI/Elements/MenuList.cpp | 7 + sources/RenderSystem/GUI/Elements/Window.cpp | 8 +- .../RenderSystem/GUI/Renderer/Renderer.cpp | 266 ++++++++++++ .../RenderSystem/GUI/Renderer/Renderer.ixx | 19 + sources/RenderSystem/GraphicsSystem.ixx | 1 + sources/Spectrum/main.cpp | 10 +- sources/Test/Tests/Test.GUI.ixx | 401 +++++++++++++++++- workdir/shaders/autogen/BackdropBlur.h | 41 ++ workdir/shaders/autogen/BackdropDraw.h | 41 ++ workdir/shaders/autogen/tables/BackdropBlur.h | 26 ++ workdir/shaders/autogen/tables/BackdropDraw.h | 28 ++ workdir/shaders/gui/backdrop_blur.hlsl | 43 ++ workdir/shaders/gui/backdrop_draw.hlsl | 46 ++ 31 files changed, 1466 insertions(+), 19 deletions(-) create mode 100644 sources/HAL/autogen/pso/UI/BackdropBlur.pso.ixx create mode 100644 sources/HAL/autogen/pso/UI/BackdropDraw.pso.ixx create mode 100644 sources/HAL/autogen/slots/UI/BackdropBlur.ixx create mode 100644 sources/HAL/autogen/slots/UI/BackdropDraw.ixx create mode 100644 sources/HAL/autogen/tables/UI/BackdropBlur.table.ixx create mode 100644 sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx create mode 100644 workdir/shaders/autogen/BackdropBlur.h create mode 100644 workdir/shaders/autogen/BackdropDraw.h create mode 100644 workdir/shaders/autogen/tables/BackdropBlur.h create mode 100644 workdir/shaders/autogen/tables/BackdropDraw.h create mode 100644 workdir/shaders/gui/backdrop_blur.hlsl create mode 100644 workdir/shaders/gui/backdrop_draw.hlsl diff --git a/sources/HAL/autogen/autogen.cpp b/sources/HAL/autogen/autogen.cpp index b8c244158..015af680f 100644 --- a/sources/HAL/autogen/autogen.cpp +++ b/sources/HAL/autogen/autogen.cpp @@ -129,6 +129,8 @@ std::optional get_slot(std::string_view slot_name) if(slot_name == "NinePatch") return SlotID::NinePatch; if(slot_name == "ColorRect") return SlotID::ColorRect; if(slot_name == "FlowGraph") return SlotID::FlowGraph; + if(slot_name == "BackdropBlur") return SlotID::BackdropBlur; + if(slot_name == "BackdropDraw") return SlotID::BackdropDraw; if(slot_name == "LineRender") return SlotID::LineRender; if(slot_name == "FrameGraph_Debug_Common") return SlotID::FrameGraph_Debug_Common; if(slot_name == "FrameGraph_Debug_Texture2D") return SlotID::FrameGraph_Debug_Texture2D; @@ -422,6 +424,10 @@ uint get_table_index(SlotID id) if(id == SlotID::FlowGraph) return Slots::UI::FlowGraph::Slot::ID; + if(id == SlotID::BackdropBlur) return Slots::UI::BackdropBlur::Slot::ID; + + if(id == SlotID::BackdropDraw) return Slots::UI::BackdropDraw::Slot::ID; + if(id == SlotID::LineRender) return Slots::UI::LineRender::Slot::ID; if(id == SlotID::FrameGraph_Debug_Common) return Slots::Dev::FrameGraph_Debug_Common::Slot::ID; @@ -640,6 +646,8 @@ std::string get_slot_name(SlotID id) if(id == SlotID::NinePatch) return "NinePatch"; if(id == SlotID::ColorRect) return "ColorRect"; if(id == SlotID::FlowGraph) return "FlowGraph"; + if(id == SlotID::BackdropBlur) return "BackdropBlur"; + if(id == SlotID::BackdropDraw) return "BackdropDraw"; if(id == SlotID::LineRender) return "LineRender"; if(id == SlotID::FrameGraph_Debug_Common) return "FrameGraph_Debug_Common"; if(id == SlotID::FrameGraph_Debug_Texture2D) return "FrameGraph_Debug_Texture2D"; diff --git a/sources/HAL/autogen/autogen.ixx b/sources/HAL/autogen/autogen.ixx index 297210773..d3ee3a3e6 100644 --- a/sources/HAL/autogen/autogen.ixx +++ b/sources/HAL/autogen/autogen.ixx @@ -311,6 +311,10 @@ export import :Autogen.Slots.UI.ColorRect; export import :Autogen.Tables.UI.ColorRect; export import :Autogen.Slots.UI.FlowGraph; export import :Autogen.Tables.UI.FlowGraph; +export import :Autogen.Slots.UI.BackdropBlur; +export import :Autogen.Tables.UI.BackdropBlur; +export import :Autogen.Slots.UI.BackdropDraw; +export import :Autogen.Tables.UI.BackdropDraw; export import :Autogen.Tables.UI.VSLine; export import :Autogen.Slots.UI.LineRender; export import :Autogen.Tables.UI.LineRender; @@ -505,6 +509,7 @@ export import :Autogen.PSO.Post.TonemapHistogramClear; export import :Autogen.PSO.Post.TonemapHistogramBuild; export import :Autogen.PSO.Post.TonemapExposureAdapt; export import :Autogen.PSO.Post.TonemapApply; +export import :Autogen.PSO.UI.BackdropBlur; export import :Autogen.PSO.Dev.FrameGraph_Debug_Texture2D; export import :Autogen.PSO.Dev.FrameGraph_Debug_Texture2DArray; export import :Autogen.PSO.Dev.FrameGraph_Debug_Texture3D; @@ -579,6 +584,7 @@ export import :Autogen.PSO.Editor.StencilerLast; export import :Autogen.PSO.UI.NinePatch; export import :Autogen.PSO.UI.SimpleRect; export import :Autogen.PSO.UI.CanvasBack; +export import :Autogen.PSO.UI.BackdropDraw; export import :Autogen.PSO.UI.CanvasLines; export import :Autogen.PSO.Dev.StatGraphLines; export import :Autogen.PSO.GI.Voxel.Dev.VoxelDebug; diff --git a/sources/HAL/autogen/enums.ixx b/sources/HAL/autogen/enums.ixx index d91a6d073..718897d17 100644 --- a/sources/HAL/autogen/enums.ixx +++ b/sources/HAL/autogen/enums.ixx @@ -104,6 +104,7 @@ export TonemapHistogramBuild, TonemapExposureAdapt, TonemapApply, + BackdropBlur, FrameGraph_Debug_Texture2D, FrameGraph_Debug_Texture2DArray, FrameGraph_Debug_Texture3D, @@ -178,6 +179,7 @@ export NinePatch, SimpleRect, CanvasBack, + BackdropDraw, CanvasLines, StatGraphLines, VoxelDebug, @@ -361,6 +363,8 @@ export NinePatch = "NinePatch"_crc32, ColorRect = "ColorRect"_crc32, FlowGraph = "FlowGraph"_crc32, + BackdropBlur = "BackdropBlur"_crc32, + BackdropDraw = "BackdropDraw"_crc32, VSLine = "VSLine"_crc32, LineRender = "LineRender"_crc32, FrameGraph_Debug_Common = "FrameGraph_Debug_Common"_crc32, diff --git a/sources/HAL/autogen/pso.cpp b/sources/HAL/autogen/pso.cpp index 9dbde8b91..e3ab3c504 100644 --- a/sources/HAL/autogen/pso.cpp +++ b/sources/HAL/autogen/pso.cpp @@ -112,6 +112,7 @@ void init_pso(HAL::Device& device, enum_array& pso) tasks.emplace_back(PSOBase::create(device, pso[PSO::TonemapHistogramBuild])); tasks.emplace_back(PSOBase::create(device, pso[PSO::TonemapExposureAdapt])); tasks.emplace_back(PSOBase::create(device, pso[PSO::TonemapApply])); + tasks.emplace_back(PSOBase::create(device, pso[PSO::BackdropBlur])); tasks.emplace_back(PSOBase::create(device, pso[PSO::FrameGraph_Debug_Texture2D])); tasks.emplace_back(PSOBase::create(device, pso[PSO::FrameGraph_Debug_Texture2DArray])); tasks.emplace_back(PSOBase::create(device, pso[PSO::FrameGraph_Debug_Texture3D])); @@ -187,6 +188,7 @@ void init_pso(HAL::Device& device, enum_array& pso) tasks.emplace_back(PSOBase::create(device, pso[PSO::NinePatch])); tasks.emplace_back(PSOBase::create(device, pso[PSO::SimpleRect])); tasks.emplace_back(PSOBase::create(device, pso[PSO::CanvasBack])); + tasks.emplace_back(PSOBase::create(device, pso[PSO::BackdropDraw])); tasks.emplace_back(PSOBase::create(device, pso[PSO::CanvasLines])); tasks.emplace_back(PSOBase::create(device, pso[PSO::StatGraphLines])); tasks.emplace_back(PSOBase::create(device, pso[PSO::VoxelDebug])); @@ -219,6 +221,7 @@ void init_pso(HAL::Device& device, enum_array& pso) //decltype(PSOS::MipMapping::NonPowerOfTwo) PSOS::MipMapping::NonPowerOfTwo; //decltype(PSOS::MipMapping::Gamma) PSOS::MipMapping::Gamma; //decltype(PSOS::MipMapping::Slices) PSOS::MipMapping::Slices; +//decltype(PSOS::BackdropBlur::Up) PSOS::BackdropBlur::Up; //decltype(PSOS::Lighting::SecondBounce) PSOS::Lighting::SecondBounce; //decltype(PSOS::VoxelDownsample::Count) PSOS::VoxelDownsample::Count; //decltype(PSOS::VSMShadowBlur::VsmRtxVerify) PSOS::VSMShadowBlur::VsmRtxVerify; diff --git a/sources/HAL/autogen/pso/UI/BackdropBlur.pso.ixx b/sources/HAL/autogen/pso/UI/BackdropBlur.pso.ixx new file mode 100644 index 000000000..a755af6e5 --- /dev/null +++ b/sources/HAL/autogen/pso/UI/BackdropBlur.pso.ixx @@ -0,0 +1,60 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.PSO.UI.BackdropBlur; + +import Core; +import :PSO; +import :Enums; +import :Types; + + +export namespace PSOS +{ + namespace UI + { + struct BackdropBlur: public PSOBase + { + struct Keys { + KeyValue Up; + GEN_DEF_COMP(Keys); + private: + SERIALIZE() + { + ar&NVP(Up); + } + }; + + GEN_COMPUTE_PSO(BackdropBlur, Up) + GEN_KEY(Up, true); + + + SimplePSO init_pso(Keys & key, std::function f) + { + static const ShaderDefine<&Keys::Up,&SimpleComputePSO::compute> Up = "BACKDROP_UP"; + + + SimplePSO mpso("BackdropBlur"); + if(f) f(mpso,key); + + mpso.root_signature = Layouts::DefaultLayout; + + mpso.compute.file_name = "shaders/gui/backdrop_blur.hlsl"; + mpso.compute.entry_point = "CS"; + mpso.compute.flags = HAL::ShaderOptions::None; + + Up.Apply(mpso, key); + return mpso; + } + + private: + SERIALIZE() + { + ar&NVP(wrap(psos)); + } + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/pso/UI/BackdropDraw.pso.ixx b/sources/HAL/autogen/pso/UI/BackdropDraw.pso.ixx new file mode 100644 index 000000000..cd1605d3f --- /dev/null +++ b/sources/HAL/autogen/pso/UI/BackdropDraw.pso.ixx @@ -0,0 +1,65 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.PSO.UI.BackdropDraw; + +import Core; +import :PSO; +import :Enums; +import :Types; + + +export namespace PSOS +{ + namespace UI + { + struct BackdropDraw: public PSOBase + { + struct Keys { + GEN_DEF_COMP(Keys); + private: + SERIALIZE() + { + } + }; + + GEN_GRAPHICS_PSO(BackdropDraw) + + + SimplePSO init_pso(Keys & key, std::function f) + { + + + SimplePSO mpso("BackdropDraw"); + if(f) f(mpso,key); + + mpso.root_signature = Layouts::DefaultLayout; + + mpso.vertex.file_name = "shaders/gui/backdrop_draw.hlsl"; + mpso.vertex.entry_point = "VS"; + mpso.vertex.flags = HAL::ShaderOptions::None; + + mpso.pixel.file_name = "shaders/gui/backdrop_draw.hlsl"; + mpso.pixel.entry_point = "PS"; + mpso.pixel.flags = HAL::ShaderOptions::None; + + + mpso.rtv_formats = { HAL::Format::R16G16B16A16_FLOAT }; + mpso.blend = { HAL::Blends::AlphaBlend }; + + mpso.cull =HAL::CullMode::None; + mpso.enable_depth =false; + return mpso; + } + + private: + SERIALIZE() + { + ar&NVP(wrap(psos)); + } + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/slots/UI/BackdropBlur.ixx b/sources/HAL/autogen/slots/UI/BackdropBlur.ixx new file mode 100644 index 000000000..f2868c4d6 --- /dev/null +++ b/sources/HAL/autogen/slots/UI/BackdropBlur.ixx @@ -0,0 +1,26 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Slots.UI.BackdropBlur; +import Core; +import :Autogen.Tables.UI.BackdropBlur; +import :Autogen.Layouts.Frame.DefaultLayout; +import :SIG; +import :Types; +import :Enums; +import :Slots; + +export namespace Slots +{ + namespace UI + { + struct BackdropBlur :public DataHolder + { + static constexpr SIG_TYPE TYPE = SIG_TYPE::Slot; + BackdropBlur() = default; + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/slots/UI/BackdropDraw.ixx b/sources/HAL/autogen/slots/UI/BackdropDraw.ixx new file mode 100644 index 000000000..612ccc720 --- /dev/null +++ b/sources/HAL/autogen/slots/UI/BackdropDraw.ixx @@ -0,0 +1,26 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Slots.UI.BackdropDraw; +import Core; +import :Autogen.Tables.UI.BackdropDraw; +import :Autogen.Layouts.Frame.DefaultLayout; +import :SIG; +import :Types; +import :Enums; +import :Slots; + +export namespace Slots +{ + namespace UI + { + struct BackdropDraw :public DataHolder + { + static constexpr SIG_TYPE TYPE = SIG_TYPE::Slot; + BackdropDraw() = default; + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/tables/UI/BackdropBlur.table.ixx b/sources/HAL/autogen/tables/UI/BackdropBlur.table.ixx new file mode 100644 index 000000000..961ecbc09 --- /dev/null +++ b/sources/HAL/autogen/tables/UI/BackdropBlur.table.ixx @@ -0,0 +1,78 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Tables.UI.BackdropBlur; + +import Core; +import :SIG; +import :Types; +import :HLSL; +import :Enums; +export namespace Table +{ + #pragma pack(push, 1) + namespace UI + { + struct BackdropBlur + { + static constexpr SlotID ID = SlotID::BackdropBlur; + uint2 dst_size; + float2 src_texel; + float offset; + HLSL::Texture2D src; + HLSL::RWTexture2D dst; + HLSL::Texture2D& GetSrc() { return src; } + HLSL::RWTexture2D& GetDst() { return dst; } + uint2& GetDst_size() { return dst_size; } + float2& GetSrc_texel() { return src_texel; } + float& GetOffset() { return offset; } + static constexpr SIG_TYPE TYPE = SIG_TYPE::Table; + template + void compile(Compiler& compiler) const + { + compiler.compile(dst_size, "BackdropBlur::dst_size"); + compiler.compile(src_texel, "BackdropBlur::src_texel"); + compiler.compile(offset, "BackdropBlur::offset"); + compiler.compile(src, "BackdropBlur::src"); + compiler.compile(dst, "BackdropBlur::dst"); + } + struct Compiled + { + uint2 dst_size; // uint2 + float2 src_texel; // float2 + float offset; // float + uint src; // Texture2D + uint dst; // RWTexture2D + + + private: + SERIALIZE() + { + ar& NVP(dst_size); + ar& NVP(src_texel); + ar& NVP(offset); + } + + + }; + + static std::string get_typename() + { + return "Tables::BackdropBlur"; + } + private: + SERIALIZE() + { + ar& NVP(dst_size); + ar& NVP(src_texel); + ar& NVP(offset); + } + + }; + } + #pragma pack(pop) +} + diff --git a/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx b/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx new file mode 100644 index 000000000..96204c502 --- /dev/null +++ b/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx @@ -0,0 +1,86 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Tables.UI.BackdropDraw; + +import Core; +import :SIG; +import :Types; +import :HLSL; +import :Enums; +export namespace Table +{ + #pragma pack(push, 1) + namespace UI + { + struct BackdropDraw + { + static constexpr SlotID ID = SlotID::BackdropDraw; + float4 rect; + float4 map; + float2 window_size; + float4 tint; + float saturation; + HLSL::Texture2D blurred; + HLSL::Texture2D& GetBlurred() { return blurred; } + float4& GetRect() { return rect; } + float4& GetMap() { return map; } + float2& GetWindow_size() { return window_size; } + float4& GetTint() { return tint; } + float& GetSaturation() { return saturation; } + static constexpr SIG_TYPE TYPE = SIG_TYPE::Table; + template + void compile(Compiler& compiler) const + { + compiler.compile(rect, "BackdropDraw::rect"); + compiler.compile(map, "BackdropDraw::map"); + compiler.compile(window_size, "BackdropDraw::window_size"); + compiler.compile(tint, "BackdropDraw::tint"); + compiler.compile(saturation, "BackdropDraw::saturation"); + compiler.compile(blurred, "BackdropDraw::blurred"); + } + struct Compiled + { + float4 rect; // float4 + float4 map; // float4 + float2 window_size; // float2 + float4 tint; // float4 + float saturation; // float + uint blurred; // Texture2D + + + private: + SERIALIZE() + { + ar& NVP(rect); + ar& NVP(map); + ar& NVP(window_size); + ar& NVP(tint); + ar& NVP(saturation); + } + + + }; + + static std::string get_typename() + { + return "Tables::BackdropDraw"; + } + private: + SERIALIZE() + { + ar& NVP(rect); + ar& NVP(map); + ar& NVP(window_size); + ar& NVP(tint); + ar& NVP(saturation); + } + + }; + } + #pragma pack(pop) +} + diff --git a/sources/Prism/defs/ui.prism b/sources/Prism/defs/ui.prism index 53a2f91a0..0332d4161 100644 --- a/sources/Prism/defs/ui.prism +++ b/sources/Prism/defs/ui.prism @@ -135,6 +135,62 @@ namespace UI blend = { AlphaBlend }; } + # One step of the backdrop blur (Dual Kawase): src mip -> dst mip. The same + # kernel pair serves every radius -- the caller picks how many down/up steps + # to run and the sample offset (fractional spread between whole octaves). + [Bind = DefaultLayout::Instance0] + struct BackdropBlur + { + Texture2D src; + RWTexture2D dst; + uint2 dst_size; + float2 src_texel; + float offset; + } + + ComputePSO BackdropBlur + { + root = DefaultLayout; + + [EntryPoint = CS] + compute = "gui/backdrop_blur.hlsl"; + + # Up: 8-tap tent upsample of the smaller mip; off: 5-tap downsample. + [rename = BACKDROP_UP] + [CS] + define Up; + } + + # Composites the blurred snapshot back over the element's rect. `map` + # turns a window-space position (the same units as `rect`) into a UV of the + # blurred texture: uv = window_pos * map.xy + map.zw. + [Bind = DefaultLayout::Instance0] + struct BackdropDraw + { + Texture2D blurred; + float4 rect; + float4 map; + float2 window_size; + float4 tint; + float saturation; + } + + GraphicsPSO BackdropDraw + { + root = DefaultLayout; + + [EntryPoint = VS] + vertex = "gui/backdrop_draw.hlsl"; + + [EntryPoint = PS] + pixel = "gui/backdrop_draw.hlsl"; + + rtv = { R16G16B16A16_FLOAT }; + blend = { AlphaBlend }; + cull = None; + enable_depth = false; + } + struct VSLine { float2 pos; diff --git a/sources/RenderSystem/GUI/Base.cpp b/sources/RenderSystem/GUI/Base.cpp index f9c69dcca..21f380478 100644 --- a/sources/RenderSystem/GUI/Base.cpp +++ b/sources/RenderSystem/GUI/Base.cpp @@ -877,6 +877,9 @@ namespace GUI if (visibility) { + if (backdrop_blur && user_ui && user_ui->backdrop_enabled) + backdrop_slot = user_ui->backdrop_counter++; + on_pre_render(c); user_ui->draw_infos.emplace_back(c.ui_clipping, entry_scissors,c.scale,this,c.offset,true); // rec_c.execute([this](Context &c) { @@ -1064,6 +1067,46 @@ namespace GUI { } + // Without a target (hosts that draw the UI elsewhere) there is nothing to + // snapshot, and elements fall back to their regular look. + bool base::backdrop_active(const Context& c) const + { + if (!(backdrop_blur && user_ui && user_ui->backdrop_enabled && c.target.resource)) + return false; + + if (backdrop_standalone_only) + for (auto p = const_cast(this)->get_parent(); p; p = p->get_parent()) + if (p->backdrop_blur) + return false; + + return true; + } + + float base::control_opacity(const Context& c) + { + for (auto p = get_parent(); p; p = p->get_parent()) + if (p->glass_children) + return p->backdrop_active(c) ? p->glass_opacity : 1.0f; + return 1.0f; + } + + void base::draw_before(Context& c) + { + if (backdrop_active(c)) + { + BackdropBlurParams params; + params.radius = user_ui->backdrop_radius; + params.saturation = user_ui->backdrop_saturation; + params.tint.w = user_ui->backdrop_tint; + + rect r = get_render_bounds(); + if (r.w > 0 && r.h > 0) + c.renderer->draw_backdrop_blur(c, backdrop_slot, r, params); + } + + draw(c); + } + void user_interface::process_ui(float delta_time) @@ -1143,6 +1186,7 @@ namespace GUI draw_infos.clear(); pre_draw_infos.clear(); + backdrop_counter = 0; draw_recursive(c); drag.draw(c); } @@ -2161,10 +2205,15 @@ void PassDefault::render( command_list->get_graphics().set_rtv(rt, rt_options); } + const float ui_scale = [&] { const auto& display = context.graph->get_context<::Table::UI::DisplayState>(); + return display.hdr ? display.sdr_white_nits / 80.0f : 1.0f; + }(); + + { Slots::UI::DisplayOutput output; - output.GetUi_scale() = display.hdr ? display.sdr_white_nits / 80.0f : 1.0f; + output.GetUi_scale() = ui_scale; command_list->get_graphics().set(output); } @@ -2172,6 +2221,8 @@ void PassDefault::render( GUIInfo c; c.renderer = &renderer; c.command_list = command_list; + c.target = texture; + c.ui_scale = ui_scale; c.delta_time = ui_ctx.dt; // data.ResultTexture, not a context lookup: need() resolved this field to // whatever the override selected (or to ResultTexture itself), so setup and @@ -2198,7 +2249,7 @@ void PassDefault::render( c.window_size = ui_ctx.scaled_size; if (e.before) - e.elem->draw(c); + e.elem->draw_before(c); else e.elem->draw_after(c); } diff --git a/sources/RenderSystem/GUI/Base.ixx b/sources/RenderSystem/GUI/Base.ixx index 863556636..3ac44325e 100644 --- a/sources/RenderSystem/GUI/Base.ixx +++ b/sources/RenderSystem/GUI/Base.ixx @@ -26,6 +26,12 @@ export float scale = 1; HAL::Texture2DView result_texture_srv; + // What the UI is drawing into. Backdrop effects snapshot it mid-pass; + // an empty view (tests, previews that render the UI elsewhere) makes + // them fall back to their flat tint. + HAL::Texture2DView target; + float ui_scale = 1; + GUIInfo() = default; GUIInfo(HAL::CommandList::ptr list); }; @@ -373,6 +379,27 @@ namespace GUI virtual void draw(Context&); // override;; virtual void draw_after(Context&);; + + // Blurs what is behind the element's render bounds, just before its + // own draw() (the draw loops call draw_before). Skin shadows are + // drawn as margins outside the bounds, so the bounds are the shape. + bool backdrop_blur = false; + // Only blur when no ancestor already does: for plain panels that are + // their own frosted surface when standalone but just part of an + // already back-blurred window otherwise. + bool backdrop_standalone_only = false; + uint32_t backdrop_slot = 0; + bool backdrop_active(const Context& c) const; + // For panels that are mostly covered by their own controls: while + // this element's blur is active, the controls under it (buttons, + // combo boxes) draw their backgrounds at glass_opacity so the blur + // shows through them instead of only through the padding. + bool glass_children = false; + float glass_opacity = 0.5f; + // Background opacity for a control: the nearest glass_children + // ancestor's glass_opacity while its blur is active, else 1. + float control_opacity(const Context& c); + void draw_before(Context& c); virtual sizer update_layout(sizer r, float scale); virtual void update_childs_layout(sizer& r, float scale); virtual void update_childs_layout_after(sizer &r, float scale); @@ -646,6 +673,21 @@ namespace GUI std::vector pre_draw_infos; std::vector draw_infos; + + // Backdrop blur (any element with backdrop_blur set). Not a + // VariableContext base: user_interface is already a GUI::base and + // its add_child would become ambiguous. + std::unique_ptr backdrop_context = VariableContext::create(L"UI Backdrop"); + Variable backdrop_enabled = { true, "Blur backgrounds", backdrop_context.get() }; + Variable backdrop_radius = { 3.0f, "Blur radius (px)", backdrop_context.get(), 0.0f, 96.0f }; + Variable backdrop_saturation = { 1.0f, "Blur saturation", backdrop_context.get(), 0.0f, 2.0f }; + Variable backdrop_tint = { 0.12f, "Blur tint strength", backdrop_context.get(), 0.0f, 1.0f }; + + // Hands each backdrop element its own snapshot slot for the frame: + // two elements recorded into different command lists must not + // share a texture. Reset when the draw list is collected. + uint32_t backdrop_counter = 0; + using ptr = s_ptr; using wptr = w_ptr; std::mutex m; diff --git a/sources/RenderSystem/GUI/Elements/Button.cpp b/sources/RenderSystem/GUI/Elements/Button.cpp index b1b597c23..975b674e5 100644 --- a/sources/RenderSystem/GUI/Elements/Button.cpp +++ b/sources/RenderSystem/GUI/Elements/Button.cpp @@ -12,12 +12,9 @@ namespace GUI if (background_style == view_style::DEFAULT) { //skin->draw(this, c); - if (is_pressed()) - c.renderer->draw(c, skin.Pressed, get_render_bounds()); - else if (is_hovered()) - c.renderer->draw(c, skin.Hover, get_render_bounds()); - else - c.renderer->draw(c, skin.Normal, get_render_bounds()); + GUI::Texture t = is_pressed() ? skin.Pressed : is_hovered() ? skin.Hover : skin.Normal; + t.mul_color.w *= control_opacity(c); + c.renderer->draw(c, t, get_render_bounds()); } if (background_style == view_style::FLAT) diff --git a/sources/RenderSystem/GUI/Elements/ComboBox.cpp b/sources/RenderSystem/GUI/Elements/ComboBox.cpp index b292e8dd5..14ac03ad0 100644 --- a/sources/RenderSystem/GUI/Elements/ComboBox.cpp +++ b/sources/RenderSystem/GUI/Elements/ComboBox.cpp @@ -109,12 +109,9 @@ bool GUI::Elements::combo_box::on_mouse_action(mouse_action action, mouse_button } void GUI::Elements::combo_box::draw(Context& c) { - if (is_pressed()) - c.renderer->draw(c, skin.Pressed, get_render_bounds()); - else if (is_hovered()) - c.renderer->draw(c, skin.Hover, get_render_bounds()); - else - c.renderer->draw(c, skin.Normal, get_render_bounds()); + GUI::Texture t = is_pressed() ? skin.Pressed : is_hovered() ? skin.Hover : skin.Normal; + t.mul_color.w *= control_opacity(c); + c.renderer->draw(c, t, get_render_bounds()); } diff --git a/sources/RenderSystem/GUI/Elements/Container.cpp b/sources/RenderSystem/GUI/Elements/Container.cpp index 3540692ae..4593523b7 100644 --- a/sources/RenderSystem/GUI/Elements/Container.cpp +++ b/sources/RenderSystem/GUI/Elements/Container.cpp @@ -59,6 +59,10 @@ namespace GUI Elements::container::container() { + backdrop_blur = true; + // Inside a window the window's own back-blur covers it; a second one here + // would blur the window's translucent skin again as a separate patch. + backdrop_standalone_only = true; contents->size = { 20, 20 }; contents->width_size = size_type::MATCH_CHILDREN; contents->height_size = size_type::MATCH_CHILDREN; diff --git a/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp b/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp index f41dcc4a6..600caf258 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp @@ -65,14 +65,22 @@ namespace class toolbar_panel : public GUI::base { public: + toolbar_panel() + { + // The buttons cover all but the 4 px padding, so they go glass too. + backdrop_blur = true; + glass_children = true; + } + void draw(GUI::base::Context& c) override { GUI::Texture shadow = Skin::get().Shadow; shadow.margins = { 12, 12, 12, 12 }; c.renderer->draw(c, shadow, get_render_bounds()); + // Translucent over the blurred backdrop, so it reads as frosted glass. GUI::Texture bg = Skin::get().FlowWindow.Inactive; - bg.mul_color = float4(22, 28, 38, 255) / 255.0f; + bg.mul_color = float4(22, 28, 38, backdrop_active(c) ? 150 : 255) / 255.0f; c.renderer->draw(c, bg, get_render_bounds()); } }; diff --git a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp index 30cdaebf3..05b5d2ce2 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp @@ -68,6 +68,7 @@ void GUI::Elements::FlowGraph::component_window::draw(Context& c) GUI::Elements::FlowGraph::component_window::component_window(canvas* canva, ::FlowGraph::Node* node, window_type type) { thinkable = true; + backdrop_blur = true; this->node = node; this->type = type; this->canva = canva; diff --git a/sources/RenderSystem/GUI/Elements/MenuList.cpp b/sources/RenderSystem/GUI/Elements/MenuList.cpp index 61fb56f3d..51c37b8df 100644 --- a/sources/RenderSystem/GUI/Elements/MenuList.cpp +++ b/sources/RenderSystem/GUI/Elements/MenuList.cpp @@ -5,12 +5,19 @@ import :Renderer; void GUI::Elements::menu_list::draw(Context& c) { + // The blurred backdrop (drawn by draw_before) replaces the skinned + // container; without a target the container stays. + if (backdrop_active(c)) + return; + c.renderer->draw_container(get_ptr(), c); } GUI::Elements::menu_list::menu_list(bool vertical) { this->vertical = vertical; + // Popups blur what is behind them; the menu strip is a flat bar. + backdrop_blur = vertical; padding = { 1, 1, 1, 1 }; docking = GUI::dock::NONE; if (!vertical) diff --git a/sources/RenderSystem/GUI/Elements/Window.cpp b/sources/RenderSystem/GUI/Elements/Window.cpp index ee920aa4e..118b4fa14 100644 --- a/sources/RenderSystem/GUI/Elements/Window.cpp +++ b/sources/RenderSystem/GUI/Elements/Window.cpp @@ -10,13 +10,19 @@ void GUI::Elements::window::set_title(std::string title) void GUI::Elements::window::draw(Context& c) { // skin->draw(this, c); - c.renderer->draw(c, skin.Active, get_render_bounds()); + // The skin's frame and title bar are opaque, which hid the blur everywhere + // but the content area; translucent while blurring so the whole window is glass. + GUI::Texture t = skin.Active; + if (backdrop_active(c)) + t.mul_color.w *= 0.6f; + c.renderer->draw(c, t, get_render_bounds()); label_text->color = pressed ? float4(200, 200, 200, 255) / 255.0f : float4(1, 1, 1, 1); } GUI::Elements::window::window() : GUI::Elements::resizable() { margin = { 20, 20, 20, 20 }; + backdrop_blur = true; clickable = false; clip_child = true; label_text.reset(new label()); diff --git a/sources/RenderSystem/GUI/Renderer/Renderer.cpp b/sources/RenderSystem/GUI/Renderer/Renderer.cpp index 435383b55..a07d952b3 100644 --- a/sources/RenderSystem/GUI/Renderer/Renderer.cpp +++ b/sources/RenderSystem/GUI/Renderer/Renderer.cpp @@ -1,11 +1,277 @@ module GUI:Renderer; import HAL; +import RenderSystem; +import Graphics; import :Skin; namespace GUI { + namespace + { + // Mip 0 of `down` is the snapshot, mips 1..levels the downsample chain. + // `up` mips 0..levels-1 are the upsample chain; mip 0 is what gets drawn. + constexpr uint32_t max_blur_levels = 6; + + struct BackdropSlot + { + int width = 0; + int height = 0; + uint32_t levels = 0; + std::shared_ptr down; + std::shared_ptr up; + }; + + class BackdropCache + { + std::mutex m; + std::vector slots; + + // A replaced texture may still be referenced by command lists the + // GPU hasn't finished; keep the last few alive instead of relying + // on every list holding its resources. + std::vector> retired; + static constexpr size_t retire_depth = 24; + + void retire(std::shared_ptr& t) + { + if (t) + retired.push_back(std::move(t)); + + if (retired.size() > retire_depth) + retired.erase(retired.begin(), retired.begin() + (retired.size() - retire_depth)); + } + + public: + static BackdropCache& get() + { + static BackdropCache cache; + return cache; + } + + BackdropSlot acquire(HAL::Device& device, uint32_t index, int width, int height) + { + std::lock_guard g(m); + + if (slots.size() <= index) + slots.resize(index + 1); + + auto& s = slots[index]; + + if (!s.down || s.width != width || s.height != height) + { + retire(s.down); + retire(s.up); + + uint32_t max_levels = 0; + for (int side = std::min(width, height); side > 1; side >>= 1) + max_levels++; + + s.levels = std::min(max_blur_levels, max_levels); + s.width = width; + s.height = height; + + auto make = [&](const char* name) + { + auto tex = std::make_shared(device, + HAL::ResourceDesc::Tex2D(HAL::Format::R16G16B16A16_FLOAT, + { (UINT)width, (UINT)height }, 1, s.levels + 1, + HAL::ResFlags::ShaderResource | HAL::ResFlags::UnorderedAccess), + HAL::TextureLayout::UNDEFINED); + tex->resource->set_name(name); + return tex; + }; + + s.down = make("ui_backdrop::down"); + s.up = make("ui_backdrop::up"); + } + + return s; + } + + void reset() + { + std::lock_guard g(m); + slots.clear(); + retired.clear(); + } + }; + + int mip_extent(int size, uint32_t mip) + { + return std::max(1, size >> mip); + } + } + + void Renderer::reset_backdrop() + { + BackdropCache::get().reset(); + } + + void Renderer::draw_backdrop_blur(base::Context& c, uint32_t slot_index, rect r, const BackdropBlurParams& p) + { + // The flat fallback keeps the element readable where there is nothing + // to snapshot. + auto flat = [&] + { + draw_color(c, float4(p.tint.x, p.tint.y, p.tint.z, 0.85f), r); + }; + + if (!c.target.resource || c.window_size.x <= 0 || c.window_size.y <= 0) + { + flat(); + return; + } + + PROFILE(L"backdrop_blur"); + + const ivec2 target_size = c.target.get_size(); + const float k = float(target_size.x) / c.window_size.x; + + // The part of the element that is actually visible. + const float2 element_lt = float2(r.pos) + c.offset; + const float2 element_rb = element_lt + float2(r.size); + const float2 visible_lt = float2::max(element_lt, float2(float(c.ui_clipping.left), float(c.ui_clipping.top))); + const float2 visible_rb = float2::min(element_rb, float2(float(c.ui_clipping.right), float(c.ui_clipping.bottom))); + + if (visible_rb.x <= visible_lt.x || visible_rb.y <= visible_lt.y) + return; + + const float radius_px = p.radius * c.scale * k; + const float pad = std::ceil(radius_px * 1.5f) + 2.0f; + + // Snapshot region in target pixels: the visible rect plus what the + // blur reaches in from outside it. + int region_left = std::max(0, int(std::floor(visible_lt.x * k - pad))); + int region_top = std::max(0, int(std::floor(visible_lt.y * k - pad))); + const int region_right = std::min(target_size.x, int(std::ceil(visible_rb.x * k + pad))); + const int region_bottom = std::min(target_size.y, int(std::ceil(visible_rb.y * k + pad))); + int region_w = region_right - region_left; + int region_h = region_bottom - region_top; + + if (region_w < 2 || region_h < 2) + { + flat(); + return; + } + + // Round the snapshot up to a multiple of 32 so elements of similar size + // (and an element being resized) share a texture size instead of + // reallocating every frame. The extra texels are real target pixels: + // the region grows right/down, and slides left/up at the target's edge. + constexpr int bucket = 32; + region_w = std::min(target_size.x, (region_w + bucket - 1) / bucket * bucket); + region_h = std::min(target_size.y, (region_h + bucket - 1) / bucket * bucket); + region_left = std::min(region_left, target_size.x - region_w); + region_top = std::min(region_top, target_size.y - region_h); + + // Everything batched so far has to land in the target before it is read. + nine_patch.flush(c); + + auto& device = RenderSystem::get().device(); + const BackdropSlot slot = BackdropCache::get().acquire(device, slot_index, region_w, region_h); + + auto& list = *c.command_list; + + { + PROFILE(L"backdrop_copy"); + list.get_copy().copy_texture(slot.down->resource, ivec3(0, 0, 0), + c.target.resource, ivec3(region_left, region_top, 0), + ivec3(region_w, region_h, 1)); + } + + // Whole octaves from the chain depth, the remainder from the sample + // offset (a Dual Kawase pass at depth N spreads about 2^N * offset texels). + uint32_t levels = 0; + float offset = 1.0f; + + if (radius_px >= 0.5f && slot.levels > 0) + { + levels = (uint32_t)std::clamp((int)std::ceil(std::log2(std::max(radius_px, 1.0f) / 2.0f)), 1, (int)slot.levels); + offset = std::clamp(radius_px / (float(1u << levels) * 1.2f), 0.5f, 3.0f); + } + + auto down_mip = [&](uint32_t mip) { return slot.down->texture_2d().create_mip(mip, list); }; + auto up_mip = [&](uint32_t mip) { return slot.up->texture_2d().create_mip(mip, list); }; + + HAL::Texture2DView blurred; + + if (levels == 0) + { + blurred = down_mip(0); + } + else + { + auto& compute = list.get_compute(); + compute.set_signature(Layouts::DefaultLayout); + + auto step = [&](const HAL::Texture2DView& src, int src_w, int src_h, + const HAL::Texture2DView& dst, int dst_w, int dst_h, bool up) + { + compute.set_pipeline(PSOS::UI::BackdropBlur::Up.Use(up)); + + Slots::UI::BackdropBlur data; + data.GetSrc() = src.texture2D; + data.GetDst() = dst.rwTexture2D; + data.GetDst_size() = uint2(dst_w, dst_h); + data.GetSrc_texel() = float2(1.0f / src_w, 1.0f / src_h); + data.GetOffset() = offset; + compute.set(data); + + compute.dispatch(ivec2(dst_w, dst_h), ivec2(8, 8)); + }; + + { + PROFILE(L"backdrop_blur_down"); + for (uint32_t m = 0; m < levels; m++) + step(down_mip(m), mip_extent(region_w, m), mip_extent(region_h, m), + down_mip(m + 1), mip_extent(region_w, m + 1), mip_extent(region_h, m + 1), false); + } + + { + PROFILE(L"backdrop_blur_up"); + step(down_mip(levels), mip_extent(region_w, levels), mip_extent(region_h, levels), + up_mip(levels - 1), mip_extent(region_w, levels - 1), mip_extent(region_h, levels - 1), true); + + for (int m = (int)levels - 2; m >= 0; m--) + step(up_mip(m + 1), mip_extent(region_w, m + 1), mip_extent(region_h, m + 1), + up_mip(m), mip_extent(region_w, m), mip_extent(region_h, m), true); + } + + blurred = up_mip(0); + } + + auto& graphics = list.get_graphics(); + + { + PROFILE(L"backdrop_rebind"); + RT::Frame::SingleColor rt; + rt.GetColor() = c.target.renderTarget; + graphics.set_rtv(rt, HAL::RTOptions::Default); + graphics.set_scissors(c.scissors); + + Slots::UI::DisplayOutput output; + output.GetUi_scale() = c.ui_scale; + graphics.set(output); + } + + { + PROFILE(L"backdrop_composite"); + Slots::UI::BackdropDraw data; + data.GetBlurred() = blurred.texture2D; + data.GetRect() = float4(visible_lt.x, visible_lt.y, visible_rb.x - visible_lt.x, visible_rb.y - visible_lt.y); + data.GetMap() = float4(k / region_w, k / region_h, -float(region_left) / region_w, -float(region_top) / region_h); + data.GetWindow_size() = float2(c.window_size.x, c.window_size.y); + data.GetTint() = p.tint; + data.GetSaturation() = p.saturation; + graphics.set(data); + + graphics.set_pipeline(); + graphics.set_topology(HAL::PrimitiveTopologyType::TRIANGLE, HAL::PrimitiveTopologyFeed::STRIP); + graphics.draw(4); + } + } void Renderer::draw_area(base::ptr obj, base::Context& c) diff --git a/sources/RenderSystem/GUI/Renderer/Renderer.ixx b/sources/RenderSystem/GUI/Renderer/Renderer.ixx index 1073ff92c..2af5b203e 100644 --- a/sources/RenderSystem/GUI/Renderer/Renderer.ixx +++ b/sources/RenderSystem/GUI/Renderer/Renderer.ixx @@ -29,6 +29,13 @@ export namespace GUI static void reset(); }; + struct BackdropBlurParams + { + float radius = 18.0f; // blur radius, px at UI scale 1 + float4 tint = { 0.02f, 0.02f, 0.03f, 0.35f }; // sRGB rgb, alpha = strength + float saturation = 1.25f; + }; + class Renderer { NinePatch nine_patch; @@ -51,6 +58,18 @@ export namespace GUI void draw(base::Context& c, GUI::Texture& item, rect r); void draw(base::Context& c, HAL::PipelineState::ptr state, rect r); + + // Draws what is behind `r` (everything already drawn into the target + // this frame), blurred, tinted and desaturated, over `r`. `slot` is the + // element's per-frame snapshot slot (user_interface::backdrop_counter): + // elements recorded into different command lists must not share one. + // Falls back to a flat translucent tint when the target can't be read + // (tests, previews rendering the UI somewhere else). + void draw_backdrop_blur(base::Context& c, uint32_t slot, rect r, const BackdropBlurParams& params); + + // Drops the snapshot textures; call before the device goes away. + static void reset_backdrop(); + Renderer(); // Without the Skin's area/virtual/container textures (and the asset system loading them): for hosts that diff --git a/sources/RenderSystem/GraphicsSystem.ixx b/sources/RenderSystem/GraphicsSystem.ixx index 7fdaf097c..affb1c24a 100644 --- a/sources/RenderSystem/GraphicsSystem.ixx +++ b/sources/RenderSystem/GraphicsSystem.ixx @@ -48,6 +48,7 @@ public: nvidia::NRD::reset(); nvidia::Streamline::reset(); GUI::NinePatch::reset(); + GUI::Renderer::reset_backdrop(); AssetRenderer::reset(); Skin::reset(); HAL::Texture::reset_manager(); diff --git a/sources/Spectrum/main.cpp b/sources/Spectrum/main.cpp index 3e3f75686..2afa2279b 100644 --- a/sources/Spectrum/main.cpp +++ b/sources/Spectrum/main.cpp @@ -775,14 +775,22 @@ namespace GUI class debug_toolbar_panel : public GUI::base { public: + debug_toolbar_panel() + { + // The combo boxes cover all but the 4 px padding, so they go glass too. + backdrop_blur = true; + glass_children = true; + } + void draw(GUI::base::Context& c) override { GUI::Texture shadow = Skin::get().Shadow; shadow.margins = { 12, 12, 12, 12 }; c.renderer->draw(c, shadow, get_render_bounds()); + // Translucent over the blurred backdrop, so it reads as frosted glass. GUI::Texture bg = Skin::get().FlowWindow.Inactive; - bg.mul_color = float4(22, 28, 38, 255) / 255.0f; + bg.mul_color = float4(22, 28, 38, backdrop_active(c) ? 150 : 255) / 255.0f; c.renderer->draw(c, bg, get_render_bounds()); } }; diff --git a/sources/Test/Tests/Test.GUI.ixx b/sources/Test/Tests/Test.GUI.ixx index 8350f7472..1b25ec1eb 100644 --- a/sources/Test/Tests/Test.GUI.ixx +++ b/sources/Test/Tests/Test.GUI.ixx @@ -539,7 +539,7 @@ export namespace Test c.window_size = ui_ctx.scaled_size; if (e.before) - e.elem->draw(c); + e.elem->draw_before(c); else e.elem->draw_after(c); } @@ -719,7 +719,7 @@ export namespace Test c.window_size = ui_ctx.scaled_size; if (e.before) - e.elem->draw(c); + e.elem->draw_before(c); else e.elem->draw_after(c); } @@ -904,4 +904,401 @@ export namespace Test list->execute_and_wait(); ASSERT_TEXTURE_EX(tex.get(), "text_engine_direct_draw", .tolerance = 24); } + + // Backdrop blur: a hard red|blue seam is drawn first, then a blurred panel + // over part of it. Inside the panel the seam must be smeared (blue leaks + // into the red side); outside it the seam must stay sharp. Checked on read- + // back pixel values, not a golden: the point is that the snapshot, the blur + // chain and the composite really ran in order on one command list. + TEST(Core.HAL, GUIRenderer_BackdropBlur) + { + THREAD_SCOPE(GUI); + auto& device = RenderSystem::get().device(); + constexpr uint WIDTH = 256, HEIGHT = 128; + + auto tex = std::make_shared(device, + HAL::ResourceDesc::Tex2D(UI_RT_FORMAT, {WIDTH, HEIGHT}, 1, 1, + HAL::ResFlags::RenderTarget), + HAL::HeapType::DEFAULT); + + auto& queue = device.get_queue(HAL::CommandListType::DIRECT); + auto list = queue->get_free_list(); + list->begin(L"GUIRenderer_BackdropBlur"); + + HAL::Texture2DView view(tex, *list); + set_ui_target(list, view, vec4(0.0f, 0.0f, 0.0f, 1.0f)); + + sizer_long full_vp; + full_vp.left = 0; + full_vp.top = 0; + full_vp.right = (long)WIDTH; + full_vp.bottom = (long)HEIGHT; + + GUI::Renderer renderer; + GUIInfo c; + c.renderer = &renderer; + c.command_list = list; + c.target = view; + c.ui_scale = 1.0f; + c.window_size = vec2((float)WIDTH, (float)HEIGHT); + c.offset = vec2(0.0f, 0.0f); + c.ui_clipping = full_vp; + c.scissors = full_vp; + c.scale = 1.0f; + c.delta_time = 0.0f; + + renderer.draw_color(c, float4(1.0f, 0.0f, 0.0f, 1.0f), rect{ vec2(0.0f, 0.0f), vec2(128.0f, 128.0f) }); + renderer.draw_color(c, float4(0.0f, 0.0f, 1.0f, 1.0f), rect{ vec2(128.0f, 0.0f), vec2(128.0f, 128.0f) }); + + GUI::BackdropBlurParams params; + params.radius = 16.0f; + params.tint = float4(0.0f, 0.0f, 0.0f, 0.0f); + params.saturation = 1.0f; + renderer.draw_backdrop_blur(c, 0, rect{ vec2(64.0f, 32.0f), vec2(128.0f, 64.0f) }, params); + renderer.flush(c); + + list->execute_and_wait(); + + auto data = readback_texture(tex.get()); + ASSERT_TRUE(data && !data->array.empty() && !data->array[0]->mips.empty()); + const auto& mip = *data->array[0]->mips[0]; + + auto half_to_float = [](uint16_t h) + { + const uint32_t e = (h >> 10) & 0x1f, m = h & 0x3ff; + float v = e == 0 ? std::ldexp(float(m), -24) + : std::ldexp(float(m | 0x400), int(e) - 25); + return (h & 0x8000) ? -v : v; + }; + + auto channel = [&](uint x, uint y, uint ch) + { + const uint16_t* row = reinterpret_cast(mip.data.data() + size_t(y) * mip.width_stride); + return half_to_float(row[x * 4 + ch]); + }; + + const uint y = 64; + + // Outside the panel: the seam is untouched. + ASSERT_TRUE(channel(40, y, 0) > 0.9f); + ASSERT_TRUE(channel(40, y, 2) < 0.05f); + ASSERT_TRUE(channel(220, y, 2) > 0.9f); + ASSERT_TRUE(channel(220, y, 0) < 0.05f); + + // Inside the panel, 8 px left of the seam: blue has leaked across, red has dimmed. + ASSERT_TRUE(channel(120, y, 2) > 0.08f); + ASSERT_TRUE(channel(120, y, 0) < 0.92f); + + // Same distance on the other side, mirrored. + ASSERT_TRUE(channel(136, y, 0) > 0.08f); + ASSERT_TRUE(channel(136, y, 2) < 0.92f); + + // Far from the seam inside the panel the blur must not wash the colours out. + ASSERT_TRUE(channel(70, y, 0) > 0.8f); + ASSERT_TRUE(channel(186, y, 2) > 0.8f); + } + + // The real menu element through the production draw path (create_graph → + // draw_infos → draw, the loop UI_Render runs): a popup menu straddling a + // hard red|blue seam must show it blurred. Its item rects are translucent + // dark plates, so without the backdrop pass the blue channel just left of + // the seam is exactly 0. + TEST(Core.HAL, GUIMenuList_BackdropBlur) + { + THREAD_SCOPE(GUI); + auto& device = RenderSystem::get().device(); + constexpr uint W = 512, H = 256; + constexpr float seam = 256.0f; + + auto tex = std::make_shared(device, + HAL::ResourceDesc::Tex2D(UI_RT_FORMAT, {W, H}, 1, 1, + HAL::ResFlags::RenderTarget), + HAL::HeapType::DEFAULT); + + GUI::user_interface ui; + ui.size = vec2((float)W, (float)H); + ui.backdrop_radius = 18.0f; // the assertions below look 8 px either side of the seam + + { + auto left = std::make_shared(); + left->color = float4(1.0f, 0.0f, 0.0f, 1.0f); + left->docking = GUI::dock::LEFT; + left->width_size = GUI::size_type::FIXED; + left->size = vec2(seam, 0.0f); + ui.add_child(left); + + auto right = std::make_shared(); + right->color = float4(0.0f, 0.0f, 1.0f, 1.0f); + right->docking = GUI::dock::FILL; + ui.add_child(right); + } + + auto menu = std::make_shared(); + menu->add_item("Open"); + menu->add_item("Save"); + menu->add_item("Close"); + ui.add_child(menu); + ui.process_ui(0.0f); + + // Centre the popup on the seam; its size is only known after a layout pass. + const rect first = menu->get_render_bounds(); + menu->pos = vec2(seam - first.w / 2.0f, 60.0f); + ui.process_ui(0.0f); + const rect mb = menu->get_render_bounds(); + ASSERT_TRUE(mb.x < seam - 16.0f && mb.x + mb.w > seam + 16.0f); + + FrameGraph::Graph graph; + ui.create_graph(graph); + device.get_queue(HAL::CommandListType::DIRECT)->signal_and_wait(); + + auto& predraw = graph.get_context(); + if (!predraw.pre_draw_infos.empty()) + { + auto& queue = device.get_queue(HAL::CommandListType::DIRECT); + auto pre_draw_list = queue->get_free_list(); + pre_draw_list->begin(L"pre_draw"); + GUI::user_interface::run_pre_draw(predraw.pre_draw_infos, pre_draw_list); + pre_draw_list->end(); + pre_draw_list->execute_and_wait(); + } + + auto& queue = device.get_queue(HAL::CommandListType::DIRECT); + auto list = queue->get_free_list(); + list->begin(L"GUIMenuList_BackdropBlur"); + + HAL::Texture2DView view(tex, *list); + set_ui_target(list, view, vec4(0.0f, 0.0f, 0.0f, 1.0f)); + + sizer_long full_vp; + full_vp.left = 0; + full_vp.top = 0; + full_vp.right = (long)W; + full_vp.bottom = (long)H; + list->get_graphics().set_scissors(full_vp); + + GUI::Renderer renderer; + GUIInfo c; + c.renderer = &renderer; + c.command_list = list; + c.target = view; + c.ui_scale = 1.0f; + c.window_size = vec2((float)W, (float)H); + c.offset = vec2(0.0f, 0.0f); + c.ui_clipping = full_vp; + c.scissors = full_vp; + c.scale = 1.0f; + c.delta_time = 0.0f; + + auto& ui_ctx = graph.get_context(); + for (auto& e : ui_ctx.draw_infos) + { + if (c.scissors != e.scissors) + { + renderer.flush(c); + list->get_graphics().set_scissors(e.scissors); + } + c.scale = e.scale; + c.ui_clipping = e.clip; + c.offset = e.offset; + c.scissors = e.scissors; + c.window_size = ui_ctx.scaled_size; + + if (e.before) + e.elem->draw_before(c); + else + e.elem->draw_after(c); + } + renderer.flush(c); + + list->execute_and_wait(); + + auto data = readback_texture(tex.get()); + ASSERT_TRUE(data && !data->array.empty() && !data->array[0]->mips.empty()); + const auto& mip = *data->array[0]->mips[0]; + + auto half_to_float = [](uint16_t h) + { + const uint32_t e = (h >> 10) & 0x1f, m = h & 0x3ff; + float v = e == 0 ? std::ldexp(float(m), -24) + : std::ldexp(float(m | 0x400), int(e) - 25); + return (h & 0x8000) ? -v : v; + }; + + auto channel = [&](uint x, uint y, uint ch) + { + const uint16_t* row = reinterpret_cast(mip.data.data() + size_t(y) * mip.width_stride); + return half_to_float(row[x * 4 + ch]); + }; + + // A row inside the first item's top padding: dark plate over the backdrop, no glyphs. + const uint y = (uint)(mb.y + 2.0f); + + // Outside the menu the seam is hard. + const uint outside = (uint)(mb.x - 6.0f); + ASSERT_TRUE(channel(outside, y, 0) > 0.9f); + ASSERT_TRUE(channel(outside, y, 2) < 0.02f); + + // Inside, either side of the seam: the other colour has leaked across the + // 0.41-transmittance plate (the plate is alpha 150/255 dark over the blur). + ASSERT_TRUE(channel((uint)seam - 8, y, 2) > 0.03f); + ASSERT_TRUE(channel((uint)seam + 8, y, 0) > 0.03f); + + // And it is a smooth transition, not a step: monotonic red falloff across the seam. + float prev = channel((uint)seam - 16, y, 0); + for (uint x = (uint)seam - 15; x <= (uint)seam + 16; x += 4) + { + const float cur = channel(x, y, 0); + ASSERT_TRUE(cur <= prev + 0.01f); + prev = cur; + } + } + + // A floating window goes through the same generic path (base::backdrop_blur + // + draw_before). Its skin body is 80% black, so only ~20% of the blurred + // backdrop shows through: without the blur the blue channel just left of the + // seam is exactly 0. + TEST(Core.HAL, GUIWindow_BackdropBlur) + { + THREAD_SCOPE(GUI); + auto& device = RenderSystem::get().device(); + constexpr uint W = 512, H = 256; + constexpr float seam = 256.0f; + + auto tex = std::make_shared(device, + HAL::ResourceDesc::Tex2D(UI_RT_FORMAT, {W, H}, 1, 1, + HAL::ResFlags::RenderTarget), + HAL::HeapType::DEFAULT); + + GUI::user_interface ui; + ui.size = vec2((float)W, (float)H); + ui.backdrop_radius = 12.0f; // wide enough to show well above the skin's 20% transmittance + + { + auto left = std::make_shared(); + left->color = float4(1.0f, 0.0f, 0.0f, 1.0f); + left->docking = GUI::dock::LEFT; + left->width_size = GUI::size_type::FIXED; + left->size = vec2(seam, 0.0f); + ui.add_child(left); + + auto right = std::make_shared(); + right->color = float4(0.0f, 0.0f, 1.0f, 1.0f); + right->docking = GUI::dock::FILL; + ui.add_child(right); + } + + auto win = std::make_shared(); + win->width_size = GUI::size_type::FIXED; + win->height_size = GUI::size_type::FIXED; + win->size = vec2(240.0f, 140.0f); + win->pos = vec2(seam - 120.0f, 50.0f); + ui.add_child(win); + ui.process_ui(0.0f); + + const rect wb = win->get_render_bounds(); + ASSERT_TRUE(wb.x < seam - 40.0f && wb.x + wb.w > seam + 40.0f); + + FrameGraph::Graph graph; + ui.create_graph(graph); + device.get_queue(HAL::CommandListType::DIRECT)->signal_and_wait(); + + auto& predraw = graph.get_context(); + if (!predraw.pre_draw_infos.empty()) + { + auto& queue = device.get_queue(HAL::CommandListType::DIRECT); + auto pre_draw_list = queue->get_free_list(); + pre_draw_list->begin(L"pre_draw"); + GUI::user_interface::run_pre_draw(predraw.pre_draw_infos, pre_draw_list); + pre_draw_list->end(); + pre_draw_list->execute_and_wait(); + } + + auto& queue = device.get_queue(HAL::CommandListType::DIRECT); + auto list = queue->get_free_list(); + list->begin(L"GUIWindow_BackdropBlur"); + + HAL::Texture2DView view(tex, *list); + set_ui_target(list, view, vec4(0.0f, 0.0f, 0.0f, 1.0f)); + + sizer_long full_vp; + full_vp.left = 0; + full_vp.top = 0; + full_vp.right = (long)W; + full_vp.bottom = (long)H; + list->get_graphics().set_scissors(full_vp); + + GUI::Renderer renderer; + GUIInfo c; + c.renderer = &renderer; + c.command_list = list; + c.target = view; + c.ui_scale = 1.0f; + c.window_size = vec2((float)W, (float)H); + c.offset = vec2(0.0f, 0.0f); + c.ui_clipping = full_vp; + c.scissors = full_vp; + c.scale = 1.0f; + c.delta_time = 0.0f; + + auto& ui_ctx = graph.get_context(); + for (auto& e : ui_ctx.draw_infos) + { + if (c.scissors != e.scissors) + { + renderer.flush(c); + list->get_graphics().set_scissors(e.scissors); + } + c.scale = e.scale; + c.ui_clipping = e.clip; + c.offset = e.offset; + c.scissors = e.scissors; + c.window_size = ui_ctx.scaled_size; + + if (e.before) + e.elem->draw_before(c); + else + e.elem->draw_after(c); + } + renderer.flush(c); + + list->execute_and_wait(); + + auto data = readback_texture(tex.get()); + ASSERT_TRUE(data && !data->array.empty() && !data->array[0]->mips.empty()); + const auto& mip = *data->array[0]->mips[0]; + + auto half_to_float = [](uint16_t h) + { + const uint32_t e = (h >> 10) & 0x1f, m = h & 0x3ff; + float v = e == 0 ? std::ldexp(float(m), -24) + : std::ldexp(float(m | 0x400), int(e) - 25); + return (h & 0x8000) ? -v : v; + }; + + auto channel = [&](uint x, uint y, uint ch) + { + const uint16_t* row = reinterpret_cast(mip.data.data() + size_t(y) * mip.width_stride); + return half_to_float(row[x * 4 + ch]); + }; + + // A row in the window body, below the title bar. + const uint y = (uint)(wb.y + 90.0f); + + // Far outside the window the seam is untouched. + ASSERT_TRUE(channel(20, y, 0) > 0.9f); + ASSERT_TRUE(channel(20, y, 2) < 0.02f); + + // Inside the body the other colour has leaked across (through the skin's ~20% transmittance). + ASSERT_TRUE(channel((uint)seam - 6, y, 2) > 0.015f); + ASSERT_TRUE(channel((uint)seam + 6, y, 0) > 0.015f); + + // A step would be exactly 0 either side of it; blur makes the transition monotonic. + float prev = channel((uint)seam - 24, y, 0); + for (uint x = (uint)seam - 20; x <= (uint)seam + 24; x += 4) + { + const float cur = channel(x, y, 0); + ASSERT_TRUE(cur <= prev + 0.005f); + prev = cur; + } + } } diff --git a/workdir/shaders/autogen/BackdropBlur.h b/workdir/shaders/autogen/BackdropBlur.h new file mode 100644 index 000000000..57fa0c066 --- /dev/null +++ b/workdir/shaders/autogen/BackdropBlur.h @@ -0,0 +1,41 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +#ifndef SLOT_4 + #define SLOT_4 +#else + #error Slot 4 is already used +#endif + +#include "layout/DefaultLayout.h" +#include "tables/BackdropBlur.h" + +#ifndef CB_DEFINED +#define CB_DEFINED +struct CB { uint offset; }; +#endif + +#ifdef __spirv__ +struct _CB_BackdropBlur { uint offset; }; +#ifdef HAL_SHADER_RECORD_BackdropBlur +// A ray tracing hit shader's local root constant: on Vulkan it is the shader +// record's data, right after the group handle in the hit table entry. +[[vk::shader_record_ext]] ConstantBuffer<_CB_BackdropBlur> pass_BackdropBlur; +#else +static _CB_BackdropBlur pass_BackdropBlur = { _hal_push.s4 }; +#endif +#else +ConstantBuffer pass_BackdropBlur: register(b4, space4); +#endif +ConstantBuffer CreateBackdropBlur() +{ + return ResourceDescriptorHeap[pass_BackdropBlur.offset]; +} + +#ifndef NO_GLOBAL +static const ConstantBuffer backdropBlur_global = CreateBackdropBlur(); +ConstantBuffer GetBackdropBlur() { return backdropBlur_global; } +#endif \ No newline at end of file diff --git a/workdir/shaders/autogen/BackdropDraw.h b/workdir/shaders/autogen/BackdropDraw.h new file mode 100644 index 000000000..e25879667 --- /dev/null +++ b/workdir/shaders/autogen/BackdropDraw.h @@ -0,0 +1,41 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +#ifndef SLOT_4 + #define SLOT_4 +#else + #error Slot 4 is already used +#endif + +#include "layout/DefaultLayout.h" +#include "tables/BackdropDraw.h" + +#ifndef CB_DEFINED +#define CB_DEFINED +struct CB { uint offset; }; +#endif + +#ifdef __spirv__ +struct _CB_BackdropDraw { uint offset; }; +#ifdef HAL_SHADER_RECORD_BackdropDraw +// A ray tracing hit shader's local root constant: on Vulkan it is the shader +// record's data, right after the group handle in the hit table entry. +[[vk::shader_record_ext]] ConstantBuffer<_CB_BackdropDraw> pass_BackdropDraw; +#else +static _CB_BackdropDraw pass_BackdropDraw = { _hal_push.s4 }; +#endif +#else +ConstantBuffer pass_BackdropDraw: register(b4, space4); +#endif +ConstantBuffer CreateBackdropDraw() +{ + return ResourceDescriptorHeap[pass_BackdropDraw.offset]; +} + +#ifndef NO_GLOBAL +static const ConstantBuffer backdropDraw_global = CreateBackdropDraw(); +ConstantBuffer GetBackdropDraw() { return backdropDraw_global; } +#endif \ No newline at end of file diff --git a/workdir/shaders/autogen/tables/BackdropBlur.h b/workdir/shaders/autogen/tables/BackdropBlur.h new file mode 100644 index 000000000..88c6f8071 --- /dev/null +++ b/workdir/shaders/autogen/tables/BackdropBlur.h @@ -0,0 +1,26 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +#pragma once +#include "sig_hlsl.hlsl" +#include "enums.h" +namespace UI +{ + struct BackdropBlur + { + uint2 dst_size; // uint2 + float2 src_texel; // float2 + float offset; // float + uint src; // Texture2D + uint dst; // RWTexture2D + uint2 GetDst_size() { return dst_size; } + float2 GetSrc_texel() { return src_texel; } + float GetOffset() { return offset; } + Texture2D GetSrc() { return ResourceDescriptorHeap[src]; } + RWTexture2D GetDst() { return ResourceDescriptorHeap[dst]; } + }; +} +using UI::BackdropBlur; \ No newline at end of file diff --git a/workdir/shaders/autogen/tables/BackdropDraw.h b/workdir/shaders/autogen/tables/BackdropDraw.h new file mode 100644 index 000000000..369c3f6ab --- /dev/null +++ b/workdir/shaders/autogen/tables/BackdropDraw.h @@ -0,0 +1,28 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +#pragma once +#include "sig_hlsl.hlsl" +#include "enums.h" +namespace UI +{ + struct BackdropDraw + { + float4 rect; // float4 + float4 map; // float4 + float2 window_size; // float2 + float4 tint; // float4 + float saturation; // float + uint blurred; // Texture2D + float4 GetRect() { return rect; } + float4 GetMap() { return map; } + float2 GetWindow_size() { return window_size; } + float4 GetTint() { return tint; } + float GetSaturation() { return saturation; } + Texture2D GetBlurred() { return ResourceDescriptorHeap[blurred]; } + }; +} +using UI::BackdropDraw; \ No newline at end of file diff --git a/workdir/shaders/gui/backdrop_blur.hlsl b/workdir/shaders/gui/backdrop_blur.hlsl new file mode 100644 index 000000000..77fe80972 --- /dev/null +++ b/workdir/shaders/gui/backdrop_blur.hlsl @@ -0,0 +1,43 @@ +#include "../autogen/BackdropBlur.h" + +// Dual Kawase blur step (Marius Bjorge, "Bandwidth-efficient rendering"). +// Down: dst is half the size of src. Up: dst is twice the size of src. +// `o` is src texel size scaled by the caller's offset, so a fractional offset +// gives radii between whole octaves without another pass. + +[numthreads(8, 8, 1)] +void CS(uint2 dtid : SV_DispatchThreadID) +{ + auto d = GetBackdropBlur(); + uint2 size = d.GetDst_size(); + + if (dtid.x >= size.x || dtid.y >= size.y) + return; + + float2 uv = (float2(dtid) + 0.5) / float2(size); + float2 o = d.GetSrc_texel() * d.GetOffset(); + Texture2D src = d.GetSrc(); + + float4 result; + +#ifdef BACKDROP_UP + result = src.SampleLevel(linearClampSampler, uv + float2(-o.x * 2.0, 0.0), 0); + result += src.SampleLevel(linearClampSampler, uv + float2(-o.x, o.y), 0) * 2.0; + result += src.SampleLevel(linearClampSampler, uv + float2(0.0, o.y * 2.0), 0); + result += src.SampleLevel(linearClampSampler, uv + float2( o.x, o.y), 0) * 2.0; + result += src.SampleLevel(linearClampSampler, uv + float2( o.x * 2.0, 0.0), 0); + result += src.SampleLevel(linearClampSampler, uv + float2( o.x, -o.y), 0) * 2.0; + result += src.SampleLevel(linearClampSampler, uv + float2(0.0, -o.y * 2.0), 0); + result += src.SampleLevel(linearClampSampler, uv + float2(-o.x, -o.y), 0) * 2.0; + result /= 12.0; +#else + result = src.SampleLevel(linearClampSampler, uv, 0) * 4.0; + result += src.SampleLevel(linearClampSampler, uv + float2(-o.x, -o.y), 0); + result += src.SampleLevel(linearClampSampler, uv + float2( o.x, -o.y), 0); + result += src.SampleLevel(linearClampSampler, uv + float2(-o.x, o.y), 0); + result += src.SampleLevel(linearClampSampler, uv + float2( o.x, o.y), 0); + result /= 8.0; +#endif + + d.GetDst()[dtid] = result; +} diff --git a/workdir/shaders/gui/backdrop_draw.hlsl b/workdir/shaders/gui/backdrop_draw.hlsl new file mode 100644 index 000000000..2176550f3 --- /dev/null +++ b/workdir/shaders/gui/backdrop_draw.hlsl @@ -0,0 +1,46 @@ +struct vs_out +{ + float4 pos : SV_POSITION; + float2 wp : TEXCOORD0; +}; + +#include "../autogen/BackdropDraw.h" +#include "display_output.hlsl" + +#ifdef BUILD_FUNC_VS +vs_out VS(uint index : SV_VERTEXID) +{ + auto d = GetBackdropDraw(); + + float2 corner = float2(index & 1, index >> 1); + float2 wp = d.GetRect().xy + corner * d.GetRect().zw; + + vs_out o; + o.wp = wp; + o.pos = float4(wp.x / d.GetWindow_size().x * 2.0 - 1.0, + 1.0 - wp.y / d.GetWindow_size().y * 2.0, + 0.5, 1.0); + return o; +} +#endif + +#ifdef BUILD_FUNC_PS +float4 PS(vs_out i) : SV_TARGET0 +{ + auto d = GetBackdropDraw(); + + float2 uv = i.wp * d.GetMap().xy + d.GetMap().zw; + + // The snapshot is a copy of the swapchain, so it is already in output + // space (scRGB, ui_scale applied); only the tint is authored sRGB. + float3 c = d.GetBlurred().SampleLevel(linearClampSampler, uv, 0).rgb; + + float luma = dot(c, float3(0.2126, 0.7152, 0.0722)); + c = lerp(luma.xxx, c, d.GetSaturation()); + + float3 tint = ui_output(float4(d.GetTint().rgb, 1.0)).rgb; + c = lerp(c, tint, d.GetTint().a); + + return float4(c, 1.0); +} +#endif From 08b6d3c3a841718928bf19b7d822e8732bfee789 Mon Sep 17 00:00:00 2001 From: cheater Date: Mon, 19 Oct 2026 19:17:48 +0300 Subject: [PATCH 2/8] wip --- CLAUDE.md | 17 ++ sources/HAL/autogen/autogen.cpp | 8 + sources/HAL/autogen/autogen.ixx | 6 + sources/HAL/autogen/enums.ixx | 4 + sources/HAL/autogen/pso.cpp | 2 + sources/HAL/autogen/pso/UI/UIFx.pso.ixx | 65 +++++++ sources/HAL/autogen/pso/UI/UIShadow.pso.ixx | 65 +++++++ sources/HAL/autogen/slots/UI/UIFx.ixx | 26 +++ sources/HAL/autogen/slots/UI/UIShadow.ixx | 26 +++ .../autogen/tables/UI/BackdropDraw.table.ixx | 12 ++ sources/HAL/autogen/tables/UI/UIFx.table.ixx | 70 +++++++ .../HAL/autogen/tables/UI/UIShadow.table.ixx | 70 +++++++ sources/Prism/defs/ui.prism | 73 +++++++ sources/RenderSystem/GUI/Base.cpp | 104 +++++++++- sources/RenderSystem/GUI/Base.ixx | 62 +++++- sources/RenderSystem/GUI/Elements/Button.cpp | 3 + .../GUI/Elements/FlowGraph/Canvas.cpp | 5 +- .../Elements/FlowGraph/ComponentWindow.cpp | 8 + .../Elements/FlowGraph/ComponentWindow.ixx | 3 + .../RenderSystem/GUI/Elements/MenuList.cpp | 4 + sources/RenderSystem/GUI/Elements/Window.cpp | 1 + .../RenderSystem/GUI/Renderer/Renderer.cpp | 93 +++++++++ .../RenderSystem/GUI/Renderer/Renderer.ixx | 26 +++ sources/Spectrum/main.cpp | 98 +++++++++- sources/Test/Tests/Test.GUI.ixx | 179 ++++++++++++++++++ workdir/shaders/autogen/UIFx.h | 41 ++++ workdir/shaders/autogen/UIShadow.h | 41 ++++ workdir/shaders/autogen/tables/BackdropDraw.h | 4 + workdir/shaders/autogen/tables/UIFx.h | 30 +++ workdir/shaders/autogen/tables/UIShadow.h | 30 +++ workdir/shaders/gui/backdrop_draw.hlsl | 21 +- workdir/shaders/gui/fx.hlsl | 87 +++++++++ workdir/shaders/gui/shadow.hlsl | 68 +++++++ 33 files changed, 1340 insertions(+), 12 deletions(-) create mode 100644 sources/HAL/autogen/pso/UI/UIFx.pso.ixx create mode 100644 sources/HAL/autogen/pso/UI/UIShadow.pso.ixx create mode 100644 sources/HAL/autogen/slots/UI/UIFx.ixx create mode 100644 sources/HAL/autogen/slots/UI/UIShadow.ixx create mode 100644 sources/HAL/autogen/tables/UI/UIFx.table.ixx create mode 100644 sources/HAL/autogen/tables/UI/UIShadow.table.ixx create mode 100644 workdir/shaders/autogen/UIFx.h create mode 100644 workdir/shaders/autogen/UIShadow.h create mode 100644 workdir/shaders/autogen/tables/UIFx.h create mode 100644 workdir/shaders/autogen/tables/UIShadow.h create mode 100644 workdir/shaders/gui/fx.hlsl create mode 100644 workdir/shaders/gui/shadow.hlsl diff --git a/CLAUDE.md b/CLAUDE.md index 614e59b5c..66c4cf3b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -270,6 +270,23 @@ themselves — say exactly which toggle(s)/GUI setting(s) to flip and to what value (e.g. "Shadow Src → RTX Ref"), and what to look for. Don't flip runtime defaults, rebuild, and run the app repeatedly to eyeball it in their place. +## Say which rule you are following + +When a rule in this file changes what you do — you ask instead of acting, +stop to ask for a visual check, don't clean up yet, decline a step — say so in +the reply itself, naming the rule by its section heading (e.g. "Per the +*Off-topic requests* rule in CLAUDE.md, …"). The user must be able to tell a +rule-driven question from an ordinary one by reading the reply, not by +guessing; reasoning that never reaches the reply doesn't count. + +## Off-topic requests + +Several sessions often run side by side, each on its own subject, and a +message can land in the wrong one. When a request has nothing to do with what +this session has been working on, ask first whether it was meant for this +session or for another one, before starting on it. Proceed once the user +confirms it belongs here. + ## Status reports End every report on a piece of work with these five sections, in this order, diff --git a/sources/HAL/autogen/autogen.cpp b/sources/HAL/autogen/autogen.cpp index 015af680f..681c87315 100644 --- a/sources/HAL/autogen/autogen.cpp +++ b/sources/HAL/autogen/autogen.cpp @@ -131,6 +131,8 @@ std::optional get_slot(std::string_view slot_name) if(slot_name == "FlowGraph") return SlotID::FlowGraph; if(slot_name == "BackdropBlur") return SlotID::BackdropBlur; if(slot_name == "BackdropDraw") return SlotID::BackdropDraw; + if(slot_name == "UIShadow") return SlotID::UIShadow; + if(slot_name == "UIFx") return SlotID::UIFx; if(slot_name == "LineRender") return SlotID::LineRender; if(slot_name == "FrameGraph_Debug_Common") return SlotID::FrameGraph_Debug_Common; if(slot_name == "FrameGraph_Debug_Texture2D") return SlotID::FrameGraph_Debug_Texture2D; @@ -428,6 +430,10 @@ uint get_table_index(SlotID id) if(id == SlotID::BackdropDraw) return Slots::UI::BackdropDraw::Slot::ID; + if(id == SlotID::UIShadow) return Slots::UI::UIShadow::Slot::ID; + + if(id == SlotID::UIFx) return Slots::UI::UIFx::Slot::ID; + if(id == SlotID::LineRender) return Slots::UI::LineRender::Slot::ID; if(id == SlotID::FrameGraph_Debug_Common) return Slots::Dev::FrameGraph_Debug_Common::Slot::ID; @@ -648,6 +654,8 @@ std::string get_slot_name(SlotID id) if(id == SlotID::FlowGraph) return "FlowGraph"; if(id == SlotID::BackdropBlur) return "BackdropBlur"; if(id == SlotID::BackdropDraw) return "BackdropDraw"; + if(id == SlotID::UIShadow) return "UIShadow"; + if(id == SlotID::UIFx) return "UIFx"; if(id == SlotID::LineRender) return "LineRender"; if(id == SlotID::FrameGraph_Debug_Common) return "FrameGraph_Debug_Common"; if(id == SlotID::FrameGraph_Debug_Texture2D) return "FrameGraph_Debug_Texture2D"; diff --git a/sources/HAL/autogen/autogen.ixx b/sources/HAL/autogen/autogen.ixx index d3ee3a3e6..c1281d257 100644 --- a/sources/HAL/autogen/autogen.ixx +++ b/sources/HAL/autogen/autogen.ixx @@ -315,6 +315,10 @@ export import :Autogen.Slots.UI.BackdropBlur; export import :Autogen.Tables.UI.BackdropBlur; export import :Autogen.Slots.UI.BackdropDraw; export import :Autogen.Tables.UI.BackdropDraw; +export import :Autogen.Slots.UI.UIShadow; +export import :Autogen.Tables.UI.UIShadow; +export import :Autogen.Slots.UI.UIFx; +export import :Autogen.Tables.UI.UIFx; export import :Autogen.Tables.UI.VSLine; export import :Autogen.Slots.UI.LineRender; export import :Autogen.Tables.UI.LineRender; @@ -584,6 +588,8 @@ export import :Autogen.PSO.Editor.StencilerLast; export import :Autogen.PSO.UI.NinePatch; export import :Autogen.PSO.UI.SimpleRect; export import :Autogen.PSO.UI.CanvasBack; +export import :Autogen.PSO.UI.UIFx; +export import :Autogen.PSO.UI.UIShadow; export import :Autogen.PSO.UI.BackdropDraw; export import :Autogen.PSO.UI.CanvasLines; export import :Autogen.PSO.Dev.StatGraphLines; diff --git a/sources/HAL/autogen/enums.ixx b/sources/HAL/autogen/enums.ixx index 718897d17..452341067 100644 --- a/sources/HAL/autogen/enums.ixx +++ b/sources/HAL/autogen/enums.ixx @@ -179,6 +179,8 @@ export NinePatch, SimpleRect, CanvasBack, + UIFx, + UIShadow, BackdropDraw, CanvasLines, StatGraphLines, @@ -365,6 +367,8 @@ export FlowGraph = "FlowGraph"_crc32, BackdropBlur = "BackdropBlur"_crc32, BackdropDraw = "BackdropDraw"_crc32, + UIShadow = "UIShadow"_crc32, + UIFx = "UIFx"_crc32, VSLine = "VSLine"_crc32, LineRender = "LineRender"_crc32, FrameGraph_Debug_Common = "FrameGraph_Debug_Common"_crc32, diff --git a/sources/HAL/autogen/pso.cpp b/sources/HAL/autogen/pso.cpp index e3ab3c504..7a3553438 100644 --- a/sources/HAL/autogen/pso.cpp +++ b/sources/HAL/autogen/pso.cpp @@ -188,6 +188,8 @@ void init_pso(HAL::Device& device, enum_array& pso) tasks.emplace_back(PSOBase::create(device, pso[PSO::NinePatch])); tasks.emplace_back(PSOBase::create(device, pso[PSO::SimpleRect])); tasks.emplace_back(PSOBase::create(device, pso[PSO::CanvasBack])); + tasks.emplace_back(PSOBase::create(device, pso[PSO::UIFx])); + tasks.emplace_back(PSOBase::create(device, pso[PSO::UIShadow])); tasks.emplace_back(PSOBase::create(device, pso[PSO::BackdropDraw])); tasks.emplace_back(PSOBase::create(device, pso[PSO::CanvasLines])); tasks.emplace_back(PSOBase::create(device, pso[PSO::StatGraphLines])); diff --git a/sources/HAL/autogen/pso/UI/UIFx.pso.ixx b/sources/HAL/autogen/pso/UI/UIFx.pso.ixx new file mode 100644 index 000000000..5716a71d9 --- /dev/null +++ b/sources/HAL/autogen/pso/UI/UIFx.pso.ixx @@ -0,0 +1,65 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.PSO.UI.UIFx; + +import Core; +import :PSO; +import :Enums; +import :Types; + + +export namespace PSOS +{ + namespace UI + { + struct UIFx: public PSOBase + { + struct Keys { + GEN_DEF_COMP(Keys); + private: + SERIALIZE() + { + } + }; + + GEN_GRAPHICS_PSO(UIFx) + + + SimplePSO init_pso(Keys & key, std::function f) + { + + + SimplePSO mpso("UIFx"); + if(f) f(mpso,key); + + mpso.root_signature = Layouts::DefaultLayout; + + mpso.vertex.file_name = "shaders/gui/fx.hlsl"; + mpso.vertex.entry_point = "VS"; + mpso.vertex.flags = HAL::ShaderOptions::None; + + mpso.pixel.file_name = "shaders/gui/fx.hlsl"; + mpso.pixel.entry_point = "PS"; + mpso.pixel.flags = HAL::ShaderOptions::None; + + + mpso.rtv_formats = { HAL::Format::R16G16B16A16_FLOAT }; + mpso.blend = { HAL::Blends::AlphaBlend }; + + mpso.cull =HAL::CullMode::None; + mpso.enable_depth =false; + return mpso; + } + + private: + SERIALIZE() + { + ar&NVP(wrap(psos)); + } + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/pso/UI/UIShadow.pso.ixx b/sources/HAL/autogen/pso/UI/UIShadow.pso.ixx new file mode 100644 index 000000000..37623602f --- /dev/null +++ b/sources/HAL/autogen/pso/UI/UIShadow.pso.ixx @@ -0,0 +1,65 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.PSO.UI.UIShadow; + +import Core; +import :PSO; +import :Enums; +import :Types; + + +export namespace PSOS +{ + namespace UI + { + struct UIShadow: public PSOBase + { + struct Keys { + GEN_DEF_COMP(Keys); + private: + SERIALIZE() + { + } + }; + + GEN_GRAPHICS_PSO(UIShadow) + + + SimplePSO init_pso(Keys & key, std::function f) + { + + + SimplePSO mpso("UIShadow"); + if(f) f(mpso,key); + + mpso.root_signature = Layouts::DefaultLayout; + + mpso.vertex.file_name = "shaders/gui/shadow.hlsl"; + mpso.vertex.entry_point = "VS"; + mpso.vertex.flags = HAL::ShaderOptions::None; + + mpso.pixel.file_name = "shaders/gui/shadow.hlsl"; + mpso.pixel.entry_point = "PS"; + mpso.pixel.flags = HAL::ShaderOptions::None; + + + mpso.rtv_formats = { HAL::Format::R16G16B16A16_FLOAT }; + mpso.blend = { HAL::Blends::AlphaBlend }; + + mpso.cull =HAL::CullMode::None; + mpso.enable_depth =false; + return mpso; + } + + private: + SERIALIZE() + { + ar&NVP(wrap(psos)); + } + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/slots/UI/UIFx.ixx b/sources/HAL/autogen/slots/UI/UIFx.ixx new file mode 100644 index 000000000..6ede277fc --- /dev/null +++ b/sources/HAL/autogen/slots/UI/UIFx.ixx @@ -0,0 +1,26 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Slots.UI.UIFx; +import Core; +import :Autogen.Tables.UI.UIFx; +import :Autogen.Layouts.Frame.DefaultLayout; +import :SIG; +import :Types; +import :Enums; +import :Slots; + +export namespace Slots +{ + namespace UI + { + struct UIFx :public DataHolder + { + static constexpr SIG_TYPE TYPE = SIG_TYPE::Slot; + UIFx() = default; + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/slots/UI/UIShadow.ixx b/sources/HAL/autogen/slots/UI/UIShadow.ixx new file mode 100644 index 000000000..27548d4ff --- /dev/null +++ b/sources/HAL/autogen/slots/UI/UIShadow.ixx @@ -0,0 +1,26 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Slots.UI.UIShadow; +import Core; +import :Autogen.Tables.UI.UIShadow; +import :Autogen.Layouts.Frame.DefaultLayout; +import :SIG; +import :Types; +import :Enums; +import :Slots; + +export namespace Slots +{ + namespace UI + { + struct UIShadow :public DataHolder + { + static constexpr SIG_TYPE TYPE = SIG_TYPE::Slot; + UIShadow() = default; + }; + } +} \ No newline at end of file diff --git a/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx b/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx index 96204c502..0708a2dff 100644 --- a/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx +++ b/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx @@ -24,6 +24,8 @@ export namespace Table float2 window_size; float4 tint; float saturation; + float luminosity; + float noise; HLSL::Texture2D blurred; HLSL::Texture2D& GetBlurred() { return blurred; } float4& GetRect() { return rect; } @@ -31,6 +33,8 @@ export namespace Table float2& GetWindow_size() { return window_size; } float4& GetTint() { return tint; } float& GetSaturation() { return saturation; } + float& GetLuminosity() { return luminosity; } + float& GetNoise() { return noise; } static constexpr SIG_TYPE TYPE = SIG_TYPE::Table; template void compile(Compiler& compiler) const @@ -40,6 +44,8 @@ export namespace Table compiler.compile(window_size, "BackdropDraw::window_size"); compiler.compile(tint, "BackdropDraw::tint"); compiler.compile(saturation, "BackdropDraw::saturation"); + compiler.compile(luminosity, "BackdropDraw::luminosity"); + compiler.compile(noise, "BackdropDraw::noise"); compiler.compile(blurred, "BackdropDraw::blurred"); } struct Compiled @@ -49,6 +55,8 @@ export namespace Table float2 window_size; // float2 float4 tint; // float4 float saturation; // float + float luminosity; // float + float noise; // float uint blurred; // Texture2D @@ -60,6 +68,8 @@ export namespace Table ar& NVP(window_size); ar& NVP(tint); ar& NVP(saturation); + ar& NVP(luminosity); + ar& NVP(noise); } @@ -77,6 +87,8 @@ export namespace Table ar& NVP(window_size); ar& NVP(tint); ar& NVP(saturation); + ar& NVP(luminosity); + ar& NVP(noise); } }; diff --git a/sources/HAL/autogen/tables/UI/UIFx.table.ixx b/sources/HAL/autogen/tables/UI/UIFx.table.ixx new file mode 100644 index 000000000..718702971 --- /dev/null +++ b/sources/HAL/autogen/tables/UI/UIFx.table.ixx @@ -0,0 +1,70 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Tables.UI.UIFx; + +import Core; +import :SIG; +import :Types; +import :HLSL; +import :Enums; +export namespace Table +{ + #pragma pack(push, 1) + namespace UI + { + struct UIFx + { + static constexpr SlotID ID = SlotID::UIFx; + float4 quad; + float4 shape; + float4 reveal; + float4 border; + float4 shine; + float2 window_size; + float radius; + float4& GetQuad() { return quad; } + float4& GetShape() { return shape; } + float4& GetReveal() { return reveal; } + float4& GetBorder() { return border; } + float4& GetShine() { return shine; } + float2& GetWindow_size() { return window_size; } + float& GetRadius() { return radius; } + static constexpr SIG_TYPE TYPE = SIG_TYPE::Table; + template + void compile(Compiler& compiler) const + { + compiler.compile(quad, "UIFx::quad"); + compiler.compile(shape, "UIFx::shape"); + compiler.compile(reveal, "UIFx::reveal"); + compiler.compile(border, "UIFx::border"); + compiler.compile(shine, "UIFx::shine"); + compiler.compile(window_size, "UIFx::window_size"); + compiler.compile(radius, "UIFx::radius"); + } + using Compiled = UIFx; + + static std::string get_typename() + { + return "Tables::UIFx"; + } + private: + SERIALIZE() + { + ar& NVP(quad); + ar& NVP(shape); + ar& NVP(reveal); + ar& NVP(border); + ar& NVP(shine); + ar& NVP(window_size); + ar& NVP(radius); + } + + }; + } + #pragma pack(pop) +} + diff --git a/sources/HAL/autogen/tables/UI/UIShadow.table.ixx b/sources/HAL/autogen/tables/UI/UIShadow.table.ixx new file mode 100644 index 000000000..d62b8f091 --- /dev/null +++ b/sources/HAL/autogen/tables/UI/UIShadow.table.ixx @@ -0,0 +1,70 @@ +// ============================================================================ +// THIS FILE IS AUTOMATICALLY GENERATED - DO NOT MANUALLY EDIT +// ============================================================================ +// Generated by Prism from .prism files in sources/Prism/defs/ +// Changes will be lost on next generation. Edit the .prism source files instead. +// ============================================================================ +export module HAL:Autogen.Tables.UI.UIShadow; + +import Core; +import :SIG; +import :Types; +import :HLSL; +import :Enums; +export namespace Table +{ + #pragma pack(push, 1) + namespace UI + { + struct UIShadow + { + static constexpr SlotID ID = SlotID::UIShadow; + float4 quad; + float4 element; + float4 shape; + float4 color; + float2 window_size; + float radius; + float sigma; + float4& GetQuad() { return quad; } + float4& GetElement() { return element; } + float4& GetShape() { return shape; } + float4& GetColor() { return color; } + float2& GetWindow_size() { return window_size; } + float& GetRadius() { return radius; } + float& GetSigma() { return sigma; } + static constexpr SIG_TYPE TYPE = SIG_TYPE::Table; + template + void compile(Compiler& compiler) const + { + compiler.compile(quad, "UIShadow::quad"); + compiler.compile(element, "UIShadow::element"); + compiler.compile(shape, "UIShadow::shape"); + compiler.compile(color, "UIShadow::color"); + compiler.compile(window_size, "UIShadow::window_size"); + compiler.compile(radius, "UIShadow::radius"); + compiler.compile(sigma, "UIShadow::sigma"); + } + using Compiled = UIShadow; + + static std::string get_typename() + { + return "Tables::UIShadow"; + } + private: + SERIALIZE() + { + ar& NVP(quad); + ar& NVP(element); + ar& NVP(shape); + ar& NVP(color); + ar& NVP(window_size); + ar& NVP(radius); + ar& NVP(sigma); + } + + }; + } + #pragma pack(pop) +} + diff --git a/sources/Prism/defs/ui.prism b/sources/Prism/defs/ui.prism index 0332d4161..0793aad32 100644 --- a/sources/Prism/defs/ui.prism +++ b/sources/Prism/defs/ui.prism @@ -173,6 +173,79 @@ namespace UI float2 window_size; float4 tint; float saturation; + # Acrylic: how far the blur's luminance is pulled toward the tint's, so + # bright and dark backdrops read evenly under the glass (Fluent's + # luminosity layer). + float luminosity; + # Acrylic: strength of the static per-pixel grain. + float noise; + } + + # Procedural soft shadow / glow (Renderer::draw_shadow). All positions and + # sizes are window pixels (the same space as BackdropDraw.rect). + [Bind = DefaultLayout::Instance0] + struct UIShadow + { + # The quad drawn: the shape grown by the blur reach. + float4 quad; + # The element's own rect: nothing is drawn inside it. + float4 element; + # The shadow's rect: element moved by the offset and grown by spread. + float4 shape; + float4 color; + float2 window_size; + float radius; + float sigma; + } + + # Cursor-driven light inside a rounded rect (Renderer::draw_fx). All + # positions and sizes are window pixels; a zero strength turns that part off. + [Bind = DefaultLayout::Instance0] + struct UIFx + { + # The quad drawn: the element's rect clipped to its parent. + float4 quad; + float4 shape; + # xy cursor, z Gaussian sigma, w strength. + float4 reveal; + # x sigma of the rim's falloff from the cursor, y strength, z rim width. + float4 border; + # x sweep phase (< 0 = off), y band half-width (fraction of the sweep), z strength. + float4 shine; + float2 window_size; + float radius; + } + + GraphicsPSO UIFx + { + root = DefaultLayout; + + [EntryPoint = VS] + vertex = "gui/fx.hlsl"; + + [EntryPoint = PS] + pixel = "gui/fx.hlsl"; + + rtv = { R16G16B16A16_FLOAT }; + blend = { AlphaBlend }; + cull = None; + enable_depth = false; + } + + GraphicsPSO UIShadow + { + root = DefaultLayout; + + [EntryPoint = VS] + vertex = "gui/shadow.hlsl"; + + [EntryPoint = PS] + pixel = "gui/shadow.hlsl"; + + rtv = { R16G16B16A16_FLOAT }; + blend = { AlphaBlend }; + cull = None; + enable_depth = false; } GraphicsPSO BackdropDraw diff --git a/sources/RenderSystem/GUI/Base.cpp b/sources/RenderSystem/GUI/Base.cpp index 21f380478..e45f3a0f9 100644 --- a/sources/RenderSystem/GUI/Base.cpp +++ b/sources/RenderSystem/GUI/Base.cpp @@ -880,6 +880,9 @@ namespace GUI if (backdrop_blur && user_ui && user_ui->backdrop_enabled) backdrop_slot = user_ui->backdrop_counter++; + if (fx.reveal || fx.border || fx.shine) + update_fx(c.delta_time); + on_pre_render(c); user_ui->draw_infos.emplace_back(c.ui_clipping, entry_scissors,c.scale,this,c.offset,true); // rec_c.execute([this](Context &c) { @@ -1074,8 +1077,9 @@ namespace GUI if (!(backdrop_blur && user_ui && user_ui->backdrop_enabled && c.target.resource)) return false; + // Raw parent pointers for the same reason as control_opacity(). if (backdrop_standalone_only) - for (auto p = const_cast(this)->get_parent(); p; p = p->get_parent()) + for (const base* p = parent; p; p = p->parent) if (p->backdrop_blur) return false; @@ -1084,7 +1088,10 @@ namespace GUI float base::control_opacity(const Context& c) { - for (auto p = get_parent(); p; p = p->get_parent()) + // Raw parent pointers, not get_parent(): that returns parent->get_ptr() + // (shared_from_this), which throws for a root not owned by a shared_ptr + // -- e.g. a user_interface on the stack in the GUI tests. + for (const base* p = parent; p; p = p->parent) if (p->glass_children) return p->backdrop_active(c) ? p->glass_opacity : 1.0f; return 1.0f; @@ -1098,13 +1105,106 @@ namespace GUI params.radius = user_ui->backdrop_radius; params.saturation = user_ui->backdrop_saturation; params.tint.w = user_ui->backdrop_tint; + params.luminosity = user_ui->backdrop_luminosity; + params.noise = user_ui->backdrop_noise; rect r = get_render_bounds(); if (r.w > 0 && r.h > 0) c.renderer->draw_backdrop_blur(c, backdrop_slot, r, params); } + // After the blur: the blur snapshot reads a little beyond the element, + // and a shadow already drawn there would be blurred into the glass. + if (user_ui && (shadow.enabled || glow.enabled)) + { + rect r = get_render_bounds(); + + if (shadow.enabled && user_ui->shadows_enabled) + { + ShadowStyle s = shadow; + s.color.w = user_ui->shadow_opacity; + s.blur = user_ui->shadow_blur; + s.offset = float2(0.0f, float(user_ui->shadow_offset)); + c.renderer->draw_shadow(c, r, s); + } + + if (glow.enabled && user_ui->glow_enabled) + { + ShadowStyle g = glow; + g.color.w *= user_ui->glow_strength; + g.blur = user_ui->glow_size; + g.offset = float2(0.0f, 0.0f); + c.renderer->draw_shadow(c, r, g); + } + } + draw(c); + + // Over the element's own draw, under its children (labels stay readable). + if (user_ui && (fx.reveal || fx.border || fx.shine) && (fx_hover > 0.003f || fx_shine >= 0.0f)) + { + FxParams p; + p.radius = fx.radius; + const vec2 mouse = user_ui->get_mouse_pos(); + p.cursor = float2(mouse.x, mouse.y); + + if (fx.reveal && user_ui->reveal_enabled) + { + p.reveal_strength = user_ui->reveal_strength * fx_hover; + p.reveal_size = user_ui->reveal_size; + } + + if (fx.border && user_ui->border_enabled) + { + p.border_strength = user_ui->border_strength * fx_hover; + p.border_size = user_ui->border_size; + } + + if (fx.shine && user_ui->shine_enabled && fx_shine >= 0.0f) + { + p.shine_phase = fx_shine; + p.shine_strength = user_ui->shine_strength; + } + + c.renderer->draw_fx(c, get_render_bounds(), p); + } + } + + void base::update_fx(float dt) + { + // Hovered when the cursor is over this element or anything inside it + // (a button's label is what the hit test finds). Raw parent pointers: + // see control_opacity(). + bool hovered_now = false; + + if (user_ui) + if (auto h = user_ui->get_hovered().lock()) + for (const base* p = h.get(); p; p = p->parent) + if (p == this) + { + hovered_now = true; + break; + } + + const float target = hovered_now ? 1.0f : 0.0f; + const float previous = fx_hover; + fx_hover += (target - fx_hover) * (1.0f - std::exp(-dt * 14.0f)); + + if (std::abs(target - fx_hover) < 0.003f) + fx_hover = target; + + if (fx.shine && user_ui) + { + if (previous <= 0.0f && fx_hover > 0.0f) + fx_shine = 0.0f; + else if (fx_shine >= 0.0f) + { + fx_shine += dt / std::max(float(user_ui->shine_duration), 0.05f); + + if (fx_shine >= 1.0f) + fx_shine = -1.0f; + } + } } diff --git a/sources/RenderSystem/GUI/Base.ixx b/sources/RenderSystem/GUI/Base.ixx index 3ac44325e..590a50e5d 100644 --- a/sources/RenderSystem/GUI/Base.ixx +++ b/sources/RenderSystem/GUI/Base.ixx @@ -161,7 +161,31 @@ namespace GUI NONE = 0, WIDTH = 1, HEIGHT = 2, ALL = 3 }; - + + + // A soft procedural shadow or glow around an element's rounded rect, + // drawn outside the shape only (see Renderer::draw_shadow). Sizes are in + // UI pixels at scale 1; color is sRGB, alpha = strength. + struct ShadowStyle + { + bool enabled = false; + float4 color = { 0, 0, 0, 0.5f }; + float2 offset = { 0, 0 }; + float blur = 12.0f; // Gaussian sigma + float spread = 0.0f; // grows the shape before blurring + float radius = 4.0f; // corner radius + }; + + // Procedural light effects on an element's rounded rect (Renderer::draw_fx), + // drawn over its own draw() and under its children. Which ones apply is per + // element; strengths and sizes come from the UI Effects settings. + struct OverlayFx + { + bool reveal = false; // soft light following the cursor while hovered + bool border = false; // lit rim, brightest near the cursor + bool shine = false; // one diagonal sweep when the hover starts + float radius = 4.0f; // corner radius + }; class base; class drag_n_drop_package @@ -390,6 +414,18 @@ namespace GUI bool backdrop_standalone_only = false; uint32_t backdrop_slot = 0; bool backdrop_active(const Context& c) const; + // Drawn by draw_before after the backdrop blur, before draw(). Only + // `enabled` and the shape (radius) are per element; strength, + // softness and offset come from the UI Effects settings. + ShadowStyle shadow; + ShadowStyle glow; + // Cursor-driven light effects, drawn by draw_before after draw(). + // fx_hover eases 0..1 with the cursor over the element or a + // descendant; fx_shine runs 0..1 after the hover starts, -1 when idle. + OverlayFx fx; + float fx_hover = 0.0f; + float fx_shine = -1.0f; + void update_fx(float dt); // For panels that are mostly covered by their own controls: while // this element's blur is active, the controls under it (buttons, // combo boxes) draw their backgrounds at glass_opacity so the blur @@ -682,6 +718,30 @@ namespace GUI Variable backdrop_radius = { 3.0f, "Blur radius (px)", backdrop_context.get(), 0.0f, 96.0f }; Variable backdrop_saturation = { 1.0f, "Blur saturation", backdrop_context.get(), 0.0f, 2.0f }; Variable backdrop_tint = { 0.12f, "Blur tint strength", backdrop_context.get(), 0.0f, 1.0f }; + // Acrylic finish: luminosity pull toward the tint, and static grain. + Variable backdrop_luminosity = { 0.25f, "Blur luminosity", backdrop_context.get(), 0.0f, 1.0f }; + Variable backdrop_noise = { 0.03f, "Blur grain", backdrop_context.get(), 0.0f, 0.2f }; + + // Procedural shadows and glow (ShadowStyle on elements). + std::unique_ptr effects_context = VariableContext::create(L"UI Effects"); + Variable shadows_enabled = { true, "Shadows", effects_context.get() }; + Variable shadow_opacity = { 0.45f, "Shadow opacity", effects_context.get(), 0.0f, 1.0f }; + Variable shadow_blur = { 10.0f, "Shadow softness (px)", effects_context.get(), 0.5f, 48.0f }; + Variable shadow_offset = { 3.0f, "Shadow offset (px)", effects_context.get(), 0.0f, 24.0f }; + Variable glow_enabled = { true, "Glow", effects_context.get() }; + Variable glow_strength = { 0.8f, "Glow strength", effects_context.get(), 0.0f, 2.0f }; + Variable glow_size = { 5.0f, "Glow size (px)", effects_context.get(), 0.5f, 32.0f }; + + // Cursor-driven light effects (OverlayFx on elements). + Variable reveal_enabled = { true, "Reveal highlight", effects_context.get() }; + Variable reveal_strength = { 0.16f, "Reveal strength", effects_context.get(), 0.0f, 1.0f }; + Variable reveal_size = { 48.0f, "Reveal size (px)", effects_context.get(), 4.0f, 200.0f }; + Variable border_enabled = { true, "Edge light", effects_context.get() }; + Variable border_strength = { 0.8f, "Edge light strength", effects_context.get(), 0.0f, 1.0f }; + Variable border_size = { 70.0f, "Edge light size (px)", effects_context.get(), 4.0f, 300.0f }; + Variable shine_enabled = { true, "Shine sweep", effects_context.get() }; + Variable shine_strength = { 0.28f, "Shine strength", effects_context.get(), 0.0f, 1.0f }; + Variable shine_duration = { 0.7f, "Shine duration (s)", effects_context.get(), 0.1f, 3.0f }; // Hands each backdrop element its own snapshot slot for the frame: // two elements recorded into different command lists must not diff --git a/sources/RenderSystem/GUI/Elements/Button.cpp b/sources/RenderSystem/GUI/Elements/Button.cpp index 975b674e5..bfaa86e12 100644 --- a/sources/RenderSystem/GUI/Elements/Button.cpp +++ b/sources/RenderSystem/GUI/Elements/Button.cpp @@ -35,6 +35,9 @@ namespace GUI button::button() { + fx.reveal = true; + fx.border = true; + fx.radius = 3.0f; clickable = true; clip_child = true; label_text.reset(new label()); diff --git a/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp b/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp index 600caf258..c1d1df8dd 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp @@ -70,14 +70,11 @@ namespace // The buttons cover all but the 4 px padding, so they go glass too. backdrop_blur = true; glass_children = true; + shadow.enabled = true; } void draw(GUI::base::Context& c) override { - GUI::Texture shadow = Skin::get().Shadow; - shadow.margins = { 12, 12, 12, 12 }; - c.renderer->draw(c, shadow, get_render_bounds()); - // Translucent over the blurred backdrop, so it reads as frosted glass. GUI::Texture bg = Skin::get().FlowWindow.Inactive; bg.mul_color = float4(22, 28, 38, backdrop_active(c) ? 150 : 255) / 255.0f; diff --git a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp index 05b5d2ce2..fd15bda7a 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp @@ -65,10 +65,18 @@ void GUI::Elements::FlowGraph::component_window::draw(Context& c) c.renderer->draw(c, texture, get_render_bounds()); } +void GUI::Elements::FlowGraph::component_window::on_pre_render(Context& c) +{ + glow.enabled = selected; + base::on_pre_render(c); +} + GUI::Elements::FlowGraph::component_window::component_window(canvas* canva, ::FlowGraph::Node* node, window_type type) { thinkable = true; backdrop_blur = true; + shadow.enabled = true; + glow.color = float4(0.35f, 0.65f, 1.0f, 1.0f); this->node = node; this->type = type; this->canva = canva; diff --git a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.ixx b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.ixx index 7f79f55b4..58d217a09 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.ixx +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.ixx @@ -68,6 +68,9 @@ export namespace GUI using wptr = w_ptr; virtual void draw(Context& c) override; + // Syncs the selection glow: `selected` is set from several + // places in canvas, so it is mirrored once per frame here. + virtual void on_pre_render(Context& c) override; component_window(canvas* canva, ::FlowGraph::Node* node, window_type type); s_ptr add_input(::FlowGraph::parameter* p); diff --git a/sources/RenderSystem/GUI/Elements/MenuList.cpp b/sources/RenderSystem/GUI/Elements/MenuList.cpp index 51c37b8df..97e012812 100644 --- a/sources/RenderSystem/GUI/Elements/MenuList.cpp +++ b/sources/RenderSystem/GUI/Elements/MenuList.cpp @@ -18,6 +18,7 @@ GUI::Elements::menu_list::menu_list(bool vertical) this->vertical = vertical; // Popups blur what is behind them; the menu strip is a flat bar. backdrop_blur = vertical; + shadow.enabled = vertical; padding = { 1, 1, 1, 1 }; docking = GUI::dock::NONE; if (!vertical) @@ -131,6 +132,9 @@ void GUI::Elements::menu_list::remove_all() GUI::Elements::menu_list_element::menu_list_element(bool on_strip) { + fx.reveal = true; + fx.border = true; + fx.radius = 0.0f; this->on_strip = on_strip; } diff --git a/sources/RenderSystem/GUI/Elements/Window.cpp b/sources/RenderSystem/GUI/Elements/Window.cpp index 118b4fa14..7add1489c 100644 --- a/sources/RenderSystem/GUI/Elements/Window.cpp +++ b/sources/RenderSystem/GUI/Elements/Window.cpp @@ -23,6 +23,7 @@ GUI::Elements::window::window() : GUI::Elements::resizable() { margin = { 20, 20, 20, 20 }; backdrop_blur = true; + shadow.enabled = true; clickable = false; clip_child = true; label_text.reset(new label()); diff --git a/sources/RenderSystem/GUI/Renderer/Renderer.cpp b/sources/RenderSystem/GUI/Renderer/Renderer.cpp index a07d952b3..f49306363 100644 --- a/sources/RenderSystem/GUI/Renderer/Renderer.cpp +++ b/sources/RenderSystem/GUI/Renderer/Renderer.cpp @@ -265,6 +265,8 @@ namespace GUI data.GetWindow_size() = float2(c.window_size.x, c.window_size.y); data.GetTint() = p.tint; data.GetSaturation() = p.saturation; + data.GetLuminosity() = p.luminosity; + data.GetNoise() = p.noise; graphics.set(data); graphics.set_pipeline(); @@ -274,6 +276,97 @@ namespace GUI } + void Renderer::draw_shadow(base::Context& c, rect r, const ShadowStyle& s) + { + if (!s.enabled || s.color.w <= 0.0f || c.window_size.x <= 0 || c.window_size.y <= 0) + return; + + PROFILE(L"ui_shadow"); + + // Same space as the backdrop composite: render bounds + draw offset, + // in window pixels; style sizes are UI pixels at scale 1. + const float k = c.scale; + const float sigma = std::max(s.blur * k, 0.5f); + const float spread = s.spread * k; + const float2 offset = s.offset * k; + + const float4 element = float4(r.x + c.offset.x, r.y + c.offset.y, r.w, r.h); + const float4 shape = float4(element.x + offset.x - spread, element.y + offset.y - spread, + element.z + 2 * spread, element.w + 2 * spread); + // 3 sigma covers >99.8% of the falloff. + const float reach = 3.0f * sigma + 1.0f; + + // Clipped to the parent's clip rect like the backdrop composite: the + // shadow reaches outside the element, and the scissor alone can be + // wider than the parent (it would spill over neighbouring UI). + const float2 quad_lt = float2::max(float2(shape.x - reach, shape.y - reach), + float2(float(c.ui_clipping.left), float(c.ui_clipping.top))); + const float2 quad_rb = float2::min(float2(shape.x + shape.z + reach, shape.y + shape.w + reach), + float2(float(c.ui_clipping.right), float(c.ui_clipping.bottom))); + if (quad_rb.x <= quad_lt.x || quad_rb.y <= quad_lt.y) + return; + const float4 quad = float4(quad_lt.x, quad_lt.y, quad_rb.x - quad_lt.x, quad_rb.y - quad_lt.y); + + // Everything batched so far goes under the shadow. + nine_patch.flush(c); + + auto& graphics = c.command_list->get_graphics(); + + Slots::UI::UIShadow data; + data.GetQuad() = quad; + data.GetElement() = element; + data.GetShape() = shape; + data.GetColor() = s.color; + data.GetWindow_size() = float2(c.window_size.x, c.window_size.y); + data.GetRadius() = s.radius * k; + data.GetSigma() = sigma; + graphics.set(data); + + graphics.set_pipeline(); + graphics.set_topology(HAL::PrimitiveTopologyType::TRIANGLE, HAL::PrimitiveTopologyFeed::STRIP); + graphics.draw(4); + } + + void Renderer::draw_fx(base::Context& c, rect r, const FxParams& p) + { + const bool shine = p.shine_phase >= 0.0f && p.shine_strength > 0.0f; + if ((p.reveal_strength <= 0.0f && p.border_strength <= 0.0f && !shine) + || c.window_size.x <= 0 || c.window_size.y <= 0) + return; + + PROFILE(L"ui_fx"); + + const float k = c.scale; + const float4 shape = float4(r.x + c.offset.x, r.y + c.offset.y, r.w, r.h); + + // Clipped to the parent like the shadow: the scissor alone can be wider. + const float2 quad_lt = float2::max(float2(shape.x, shape.y), + float2(float(c.ui_clipping.left), float(c.ui_clipping.top))); + const float2 quad_rb = float2::min(float2(shape.x + shape.z, shape.y + shape.w), + float2(float(c.ui_clipping.right), float(c.ui_clipping.bottom))); + if (quad_rb.x <= quad_lt.x || quad_rb.y <= quad_lt.y) + return; + + // The element's own fill is batched; the light goes over it. + nine_patch.flush(c); + + auto& graphics = c.command_list->get_graphics(); + + Slots::UI::UIFx data; + data.GetQuad() = float4(quad_lt.x, quad_lt.y, quad_rb.x - quad_lt.x, quad_rb.y - quad_lt.y); + data.GetShape() = shape; + data.GetReveal() = float4(p.cursor.x, p.cursor.y, p.reveal_size * k, p.reveal_strength); + data.GetBorder() = float4(p.border_size * k, p.border_strength, p.border_width * k, 0.0f); + data.GetShine() = float4(shine ? p.shine_phase : -1.0f, p.shine_width, p.shine_strength, 0.0f); + data.GetWindow_size() = float2(c.window_size.x, c.window_size.y); + data.GetRadius() = p.radius * k; + graphics.set(data); + + graphics.set_pipeline(); + graphics.set_topology(HAL::PrimitiveTopologyType::TRIANGLE, HAL::PrimitiveTopologyFeed::STRIP); + graphics.draw(4); + } + void Renderer::draw_area(base::ptr obj, base::Context& c) { draw(c, area_tex, obj->get_render_bounds()); diff --git a/sources/RenderSystem/GUI/Renderer/Renderer.ixx b/sources/RenderSystem/GUI/Renderer/Renderer.ixx index 2af5b203e..0cf8ef0e1 100644 --- a/sources/RenderSystem/GUI/Renderer/Renderer.ixx +++ b/sources/RenderSystem/GUI/Renderer/Renderer.ixx @@ -34,6 +34,22 @@ export namespace GUI float radius = 18.0f; // blur radius, px at UI scale 1 float4 tint = { 0.02f, 0.02f, 0.03f, 0.35f }; // sRGB rgb, alpha = strength float saturation = 1.25f; + float luminosity = 0.0f; // 0..1, pull toward the tint's luminance + float noise = 0.0f; // grain strength + }; + + struct FxParams + { + float2 cursor = { 0, 0 }; // window pixels + float radius = 4.0f; // corner radius, UI px at scale 1 + float reveal_strength = 0.0f; + float reveal_size = 48.0f; // Gaussian sigma, UI px + float border_strength = 0.0f; + float border_size = 70.0f; // Gaussian sigma of the rim's falloff from the cursor, UI px + float border_width = 1.5f; // rim thickness, UI px + float shine_phase = -1.0f; // 0..1 sweep position, < 0 off + float shine_strength = 0.0f; + float shine_width = 0.18f; // band half-width as a fraction of the sweep length }; class Renderer @@ -59,6 +75,11 @@ export namespace GUI void draw(base::Context& c, GUI::Texture& item, rect r); void draw(base::Context& c, HAL::PipelineState::ptr state, rect r); + // Soft shadow/glow around the rounded rect r (render-bounds space, like + // draw()), drawn outside the shape only so translucent elements on top + // are not darkened. Sizes in s are UI pixels at scale 1. + void draw_shadow(base::Context& c, rect r, const ShadowStyle& s); + // Draws what is behind `r` (everything already drawn into the target // this frame), blurred, tinted and desaturated, over `r`. `slot` is the // element's per-frame snapshot slot (user_interface::backdrop_counter): @@ -67,6 +88,11 @@ export namespace GUI // (tests, previews rendering the UI somewhere else). void draw_backdrop_blur(base::Context& c, uint32_t slot, rect r, const BackdropBlurParams& params); + // Cursor-driven light over `r` (a rounded rect), inside the shape only: a + // soft spot at the cursor, a lit rim brightest near it, and a diagonal + // sweep. A strength of 0 (shine: phase < 0) turns that part off. + void draw_fx(base::Context& c, rect r, const FxParams& params); + // Drops the snapshot textures; call before the device goes away. static void reset_backdrop(); diff --git a/sources/Spectrum/main.cpp b/sources/Spectrum/main.cpp index 2afa2279b..4ab4ad00e 100644 --- a/sources/Spectrum/main.cpp +++ b/sources/Spectrum/main.cpp @@ -780,14 +780,11 @@ class debug_toolbar_panel : public GUI::base // The combo boxes cover all but the 4 px padding, so they go glass too. backdrop_blur = true; glass_children = true; + shadow.enabled = true; } void draw(GUI::base::Context& c) override { - GUI::Texture shadow = Skin::get().Shadow; - shadow.margins = { 12, 12, 12, 12 }; - c.renderer->draw(c, shadow, get_render_bounds()); - // Translucent over the blurred backdrop, so it reads as frosted glass. GUI::Texture bg = Skin::get().FlowWindow.Inactive; bg.mul_color = float4(22, 28, 38, backdrop_active(c) ? 150 : 255) / 255.0f; @@ -2061,6 +2058,99 @@ class GraphRender : public Window, public GUI::user_interface docker->get_tabs()->add_page("Widgets", split); }; + // The cursor-driven light effects (Properties > UI Effects) on plain + // cards, so each one can be looked at alone and together. + test->add_item("Effects")->on_click = [this](GUI::Elements::menu_list_element::ptr) + { + using namespace GUI::Elements; + + GUI::base::ptr page(new GUI::base()); + page->docking = GUI::dock::FILL; + page->padding = { 16, 16, 16, 16 }; + + auto hint = std::make_shared