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 b8c244158..b7463cea4 100644 --- a/sources/HAL/autogen/autogen.cpp +++ b/sources/HAL/autogen/autogen.cpp @@ -129,6 +129,11 @@ 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 == "UIShadow") return SlotID::UIShadow; + if(slot_name == "UIFx") return SlotID::UIFx; + if(slot_name == "UILayerRestore") return SlotID::UILayerRestore; 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 +427,16 @@ 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::UIShadow) return Slots::UI::UIShadow::Slot::ID; + + if(id == SlotID::UIFx) return Slots::UI::UIFx::Slot::ID; + + if(id == SlotID::UILayerRestore) return Slots::UI::UILayerRestore::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 +655,11 @@ 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::UIShadow) return "UIShadow"; + if(id == SlotID::UIFx) return "UIFx"; + if(id == SlotID::UILayerRestore) return "UILayerRestore"; 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..f7efe0248 100644 --- a/sources/HAL/autogen/autogen.ixx +++ b/sources/HAL/autogen/autogen.ixx @@ -311,6 +311,16 @@ 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.Slots.UI.UIShadow; +export import :Autogen.Tables.UI.UIShadow; +export import :Autogen.Slots.UI.UIFx; +export import :Autogen.Tables.UI.UIFx; +export import :Autogen.Slots.UI.UILayerRestore; +export import :Autogen.Tables.UI.UILayerRestore; export import :Autogen.Tables.UI.VSLine; export import :Autogen.Slots.UI.LineRender; export import :Autogen.Tables.UI.LineRender; @@ -505,6 +515,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 +590,10 @@ 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.UILayerRestore; +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; export import :Autogen.PSO.GI.Voxel.Dev.VoxelDebug; diff --git a/sources/HAL/autogen/enums.ixx b/sources/HAL/autogen/enums.ixx index d91a6d073..8174ec00e 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,10 @@ export NinePatch, SimpleRect, CanvasBack, + UILayerRestore, + UIFx, + UIShadow, + BackdropDraw, CanvasLines, StatGraphLines, VoxelDebug, @@ -361,6 +366,11 @@ export NinePatch = "NinePatch"_crc32, ColorRect = "ColorRect"_crc32, FlowGraph = "FlowGraph"_crc32, + BackdropBlur = "BackdropBlur"_crc32, + BackdropDraw = "BackdropDraw"_crc32, + UIShadow = "UIShadow"_crc32, + UIFx = "UIFx"_crc32, + UILayerRestore = "UILayerRestore"_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..0b7c070d1 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,10 @@ 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::UILayerRestore])); + 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])); tasks.emplace_back(PSOBase::create(device, pso[PSO::VoxelDebug])); @@ -219,6 +224,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; @@ -228,4 +234,5 @@ void init_pso(HAL::Device& device, enum_array& pso) //decltype(PSOS::GBufferDraw::CaptureMeshlets) PSOS::GBufferDraw::CaptureMeshlets; //decltype(PSOS::GBufferDraw::ClusterLod) PSOS::GBufferDraw::ClusterLod; //decltype(PSOS::DepthDraw::HiZOcclusion) PSOS::DepthDraw::HiZOcclusion; -//decltype(PSOS::Voxelization::Dynamic) PSOS::Voxelization::Dynamic; \ No newline at end of file +//decltype(PSOS::Voxelization::Dynamic) PSOS::Voxelization::Dynamic; +//decltype(PSOS::BackdropDraw::Direct) PSOS::BackdropDraw::Direct; \ No newline at end of file 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..985e27d26 --- /dev/null +++ b/sources/HAL/autogen/pso/UI/BackdropDraw.pso.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.PSO.UI.BackdropDraw; + +import Core; +import :PSO; +import :Enums; +import :Types; + + +export namespace PSOS +{ + namespace UI + { + struct BackdropDraw: public PSOBase + { + struct Keys { + KeyValue Direct; + GEN_DEF_COMP(Keys); + private: + SERIALIZE() + { + ar&NVP(Direct); + } + }; + + GEN_GRAPHICS_PSO(BackdropDraw, Direct) + GEN_KEY(Direct, true); + + + SimplePSO init_pso(Keys & key, std::function f) + { + static const ShaderDefine<&Keys::Direct,&SimpleGraphicsPSO::pixel> Direct = "BACKDROP_DIRECT"; + + + 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; + + Direct.Apply(mpso, key); + + 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/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/UILayerRestore.pso.ixx b/sources/HAL/autogen/pso/UI/UILayerRestore.pso.ixx new file mode 100644 index 000000000..92ae4ae01 --- /dev/null +++ b/sources/HAL/autogen/pso/UI/UILayerRestore.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.UILayerRestore; + +import Core; +import :PSO; +import :Enums; +import :Types; + + +export namespace PSOS +{ + namespace UI + { + struct UILayerRestore: public PSOBase + { + struct Keys { + GEN_DEF_COMP(Keys); + private: + SERIALIZE() + { + } + }; + + GEN_GRAPHICS_PSO(UILayerRestore) + + + SimplePSO init_pso(Keys & key, std::function f) + { + + + SimplePSO mpso("UILayerRestore"); + if(f) f(mpso,key); + + mpso.root_signature = Layouts::DefaultLayout; + + mpso.vertex.file_name = "shaders/gui/layer_restore.hlsl"; + mpso.vertex.entry_point = "VS"; + mpso.vertex.flags = HAL::ShaderOptions::None; + + mpso.pixel.file_name = "shaders/gui/layer_restore.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/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/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/UILayerRestore.ixx b/sources/HAL/autogen/slots/UI/UILayerRestore.ixx new file mode 100644 index 000000000..d3de46b73 --- /dev/null +++ b/sources/HAL/autogen/slots/UI/UILayerRestore.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.UILayerRestore; +import Core; +import :Autogen.Tables.UI.UILayerRestore; +import :Autogen.Layouts.Frame.DefaultLayout; +import :SIG; +import :Types; +import :Enums; +import :Slots; + +export namespace Slots +{ + namespace UI + { + struct UILayerRestore :public DataHolder + { + static constexpr SIG_TYPE TYPE = SIG_TYPE::Slot; + UILayerRestore() = 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/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..766e5c97e --- /dev/null +++ b/sources/HAL/autogen/tables/UI/BackdropDraw.table.ixx @@ -0,0 +1,104 @@ +// ============================================================================ +// 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; + float luminosity; + float noise; + float4 direct; + 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; } + float& GetLuminosity() { return luminosity; } + float& GetNoise() { return noise; } + float4& GetDirect() { return direct; } + 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(luminosity, "BackdropDraw::luminosity"); + compiler.compile(noise, "BackdropDraw::noise"); + compiler.compile(direct, "BackdropDraw::direct"); + compiler.compile(blurred, "BackdropDraw::blurred"); + } + struct Compiled + { + float4 rect; // float4 + float4 map; // float4 + float2 window_size; // float2 + float4 tint; // float4 + float saturation; // float + float luminosity; // float + float noise; // float + float4 direct; // float4 + uint blurred; // Texture2D + + + private: + SERIALIZE() + { + ar& NVP(rect); + ar& NVP(map); + ar& NVP(window_size); + ar& NVP(tint); + ar& NVP(saturation); + ar& NVP(luminosity); + ar& NVP(noise); + ar& NVP(direct); + } + + + }; + + 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); + ar& NVP(luminosity); + ar& NVP(noise); + ar& NVP(direct); + } + + }; + } + #pragma pack(pop) +} + 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..a3d9d6daf --- /dev/null +++ b/sources/HAL/autogen/tables/UI/UIFx.table.ixx @@ -0,0 +1,90 @@ +// ============================================================================ +// 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; + float4 ripple; + float4 focus; + float4 focus_color; + float4 inner; + float4 light; + 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; } + float4& GetRipple() { return ripple; } + float4& GetFocus() { return focus; } + float4& GetFocus_color() { return focus_color; } + float4& GetInner() { return inner; } + float4& GetLight() { return light; } + 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(ripple, "UIFx::ripple"); + compiler.compile(focus, "UIFx::focus"); + compiler.compile(focus_color, "UIFx::focus_color"); + compiler.compile(inner, "UIFx::inner"); + compiler.compile(light, "UIFx::light"); + 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(ripple); + ar& NVP(focus); + ar& NVP(focus_color); + ar& NVP(inner); + ar& NVP(light); + ar& NVP(window_size); + ar& NVP(radius); + } + + }; + } + #pragma pack(pop) +} + diff --git a/sources/HAL/autogen/tables/UI/UILayerRestore.table.ixx b/sources/HAL/autogen/tables/UI/UILayerRestore.table.ixx new file mode 100644 index 000000000..240ab11a2 --- /dev/null +++ b/sources/HAL/autogen/tables/UI/UILayerRestore.table.ixx @@ -0,0 +1,80 @@ +// ============================================================================ +// 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.UILayerRestore; + +import Core; +import :SIG; +import :Types; +import :HLSL; +import :Enums; +export namespace Table +{ + #pragma pack(push, 1) + namespace UI + { + struct UILayerRestore + { + static constexpr SlotID ID = SlotID::UILayerRestore; + float4 quad; + float2 window_size; + float2 origin; + float alpha; + HLSL::Texture2D snapshot; + HLSL::Texture2D& GetSnapshot() { return snapshot; } + float4& GetQuad() { return quad; } + float2& GetWindow_size() { return window_size; } + float2& GetOrigin() { return origin; } + float& GetAlpha() { return alpha; } + static constexpr SIG_TYPE TYPE = SIG_TYPE::Table; + template + void compile(Compiler& compiler) const + { + compiler.compile(quad, "UILayerRestore::quad"); + compiler.compile(window_size, "UILayerRestore::window_size"); + compiler.compile(origin, "UILayerRestore::origin"); + compiler.compile(alpha, "UILayerRestore::alpha"); + compiler.compile(snapshot, "UILayerRestore::snapshot"); + } + struct Compiled + { + float4 quad; // float4 + float2 window_size; // float2 + float2 origin; // float2 + float alpha; // float + uint snapshot; // Texture2D + + + private: + SERIALIZE() + { + ar& NVP(quad); + ar& NVP(window_size); + ar& NVP(origin); + ar& NVP(alpha); + } + + + }; + + static std::string get_typename() + { + return "Tables::UILayerRestore"; + } + private: + SERIALIZE() + { + ar& NVP(quad); + ar& NVP(window_size); + ar& NVP(origin); + ar& NVP(alpha); + } + + }; + } + #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 53a2f91a0..77504b5b9 100644 --- a/sources/Prism/defs/ui.prism +++ b/sources/Prism/defs/ui.prism @@ -135,6 +135,186 @@ 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; + # 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; + # Direct mode (small radii): xy = one texel of `blurred` in uv, z = tap + # spacing in texels. `blurred` is then the raw snapshot, filtered here. + float4 direct; + } + + # 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; + # xy origin (window px), z phase (< 0 = off), w strength. A wave expanding from the origin. + float4 ripple; + # x ring width (px), y strength. Drawn in focus_color (sRGB rgb) just inside the edge. + float4 focus; + float4 focus_color; + # x size (px), y strength, z vertical offset (px). Darkening inside the edge, deepest at the top. + float4 inner; + # sRGB rgb of the reveal / rim / shine light (white on dark surfaces). + float4 light; + float2 window_size; + float radius; + } + + # Group opacity (Renderer::begin_layer / end_layer): the target region as it was + # when the group began, drawn back over the finished group with alpha + # (1 - opacity). That is opacity * group + (1 - opacity) * what was below, for + # any content, without rendering the group offscreen. + [Bind = DefaultLayout::Instance0] + struct UILayerRestore + { + Texture2D snapshot; + # The quad drawn, in window pixels. + float4 quad; + float2 window_size; + # Target pixel of the snapshot's texel (0, 0). + float2 origin; + float alpha; + } + + GraphicsPSO UILayerRestore + { + root = DefaultLayout; + + [EntryPoint = VS] + vertex = "gui/layer_restore.hlsl"; + + [EntryPoint = PS] + pixel = "gui/layer_restore.hlsl"; + + rtv = { R16G16B16A16_FLOAT }; + blend = { AlphaBlend }; + cull = None; + enable_depth = false; + } + + 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 + { + root = DefaultLayout; + + [EntryPoint = VS] + vertex = "gui/backdrop_draw.hlsl"; + + [EntryPoint = PS] + pixel = "gui/backdrop_draw.hlsl"; + + # Small radii: filter the snapshot right here with a 5x5 binomial kernel + # instead of running the blur chain -- same look at the default radius, + # and no compute dispatches or UAV transitions per element. + [rename = BACKDROP_DIRECT] + [PS] + define Direct; + + rtv = { R16G16B16A16_FLOAT }; + blend = { AlphaBlend }; + cull = None; + enable_depth = false; + } + struct VSLine { float2 pos; diff --git a/sources/RenderSystem/Font/TextEngine.cpp b/sources/RenderSystem/Font/TextEngine.cpp index c1810ff89..86e6c999b 100644 --- a/sources/RenderSystem/Font/TextEngine.cpp +++ b/sources/RenderSystem/Font/TextEngine.cpp @@ -118,6 +118,22 @@ namespace Text { { IconBuilder i(icons, "chevron_right"); i.polygon({ {5.5f, 3}, {10.5f, 8}, {5.5f, 13}, {4, 11.5f}, {7.5f, 8}, {4, 4.5f} }); } { IconBuilder i(icons, "chevron_down"); i.polygon({ {3, 5.5f}, {4.5f, 4}, {8, 7.5f}, {11.5f, 4}, {13, 5.5f}, {8, 10.5f} }); } + { + // chevron_right turned clockwise about the grid centre in 10-degree + // steps, chevron_0 (right) to chevron_90 (down): a tree arrow that + // rotates as its branch opens. Pre-built rather than rotated per + // frame so the atlas keeps one small entry per step. + static std::string names[10]; + for (int step = 0; step < 10; step++) + { + names[step] = "chevron_" + std::to_string(step * 10); + const float a = step * 10.0f * 3.14159265f / 180.0f; + const float c = std::cos(a), s = std::sin(a); + auto turn = [&](float x, float y) { const float dx = x - 8, dy = y - 8; return skb_vec2_t{ 8 + dx * c - dy * s, 8 + dx * s + dy * c }; }; + IconBuilder i(icons, names[step].c_str()); + i.polygon({ turn(5.5f, 3), turn(10.5f, 8), turn(5.5f, 13), turn(4, 11.5f), turn(7.5f, 8), turn(4, 4.5f) }); + } + } { IconBuilder i(icons, "close"); i.polygon({ {3.5f, 4.9f}, {4.9f, 3.5f}, {12.5f, 11.1f}, {11.1f, 12.5f} }); diff --git a/sources/RenderSystem/GUI/Base.cpp b/sources/RenderSystem/GUI/Base.cpp index f9c69dcca..d7b79915c 100644 --- a/sources/RenderSystem/GUI/Base.cpp +++ b/sources/RenderSystem/GUI/Base.cpp @@ -53,6 +53,13 @@ namespace GUI map_to_pixels = map_to_pixels && base_parent->map_to_pixels; need_update_layout = true; + if (fade_on_add && user_ui && user_ui->fade_enabled && user_ui->window_animations) + { + fade_closing = false; + layer_opacity = 0.0f; + fade_to(1.0f, user_ui->fade_in_duration); + } + if (user_ui && thinkable) user_ui->thinking.insert(this); @@ -849,7 +856,8 @@ namespace GUI bool visibility = true; - rect r_bounds = render_bounds.get(); + const rect collected_bounds = render_bounds.get(); + rect r_bounds = collected_bounds; r_bounds.x += c.offset.x; r_bounds.y += c.offset.y; @@ -875,10 +883,29 @@ namespace GUI if (self_scissor.has_value()) entry_scissors = intersect(entry_scissors, math::convert(*self_scissor)); + // Decided once per frame here, so the before and after entries agree. + bool layered = false; + uint32_t layer_index = 0; + float layer_alpha = 1.0f; + if (visibility) { + update_fade(c.delta_time); + layered = layer_wanted(); + layer_alpha = layer_opacity; + + if (layered) + layer_index = user_ui->layer_counter++; + + for (auto& e : effects) + if (e->enabled) + { + e->collect(*this); + e->update(*this, c.delta_time); + } + on_pre_render(c); - user_ui->draw_infos.emplace_back(c.ui_clipping, entry_scissors,c.scale,this,c.offset,true); + user_ui->draw_infos.emplace_back(c.ui_clipping, entry_scissors,c.scale,this,c.offset,true,layered,layer_index,layer_alpha,collected_bounds); // rec_c.execute([this](Context &c) { // draw(c); // }); @@ -927,7 +954,7 @@ namespace GUI // if (visibility) // draw_after(c); - user_ui->draw_infos.emplace_back(c.ui_clipping, entry_scissors,c.scale, this, c.offset, false); + user_ui->draw_infos.emplace_back(c.ui_clipping, entry_scissors,c.scale, this, c.offset, false, layered, layer_index, layer_alpha, collected_bounds); /*rec_c.execute([this](Context &c) { draw_after(c); @@ -1009,8 +1036,31 @@ namespace GUI return package; } + namespace + { + // Set by draw_bounds_scope on the thread that is drawing an element. + thread_local const base* draw_bounds_owner = nullptr; + thread_local rect draw_bounds_value; + } + + draw_bounds_scope::draw_bounds_scope(const base* owner, const rect& bounds) + : previous_owner(draw_bounds_owner), previous_bounds(draw_bounds_value) + { + draw_bounds_owner = owner; + draw_bounds_value = bounds; + } + + draw_bounds_scope::~draw_bounds_scope() + { + draw_bounds_owner = previous_owner; + draw_bounds_value = previous_bounds; + } + rect base::get_render_bounds() { + if (draw_bounds_owner == this) + return draw_bounds_value; + return render_bounds.get(); } @@ -1064,6 +1114,370 @@ 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 + { + auto* blur = find_effect(); + return blur && blur->active(*this, c); + } + + bool BackdropBlur::active(const base& e, const GUIInfo& c) const + { + if (!(enabled && e.user_ui && e.user_ui->backdrop_enabled && c.target.resource)) + return false; + + // Raw parent pointers for the same reason as control_opacity(). + if (standalone_only) + for (const base* p = e.parent; p; p = p->parent) + if (p->find_effect()) + return false; + + return true; + } + + float base::control_opacity(const Context& c) + { + // 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 (auto* blur = p->find_effect(); blur && blur->glass_children) + return blur->active(*p, c) ? blur->glass_opacity : 1.0f; + return 1.0f; + } + + void base::fade_to(float target, float seconds) + { + on_fade_end = nullptr; // a new fade replaces whatever the old one was going to do + fade_from = layer_opacity; + fade_target = target; + fade_duration = std::max(seconds, 0.001f); + fade_t = 0.0f; + } + + void base::update_fade(float dt) + { + if (fade_t >= 1.0f) + return; + + fade_t = std::min(1.0f, fade_t + dt / fade_duration); + const float smooth = fade_t * fade_t * (3.0f - 2.0f * fade_t); + layer_opacity = fade_from + (fade_target - fade_from) * smooth; + + if (fade_t >= 1.0f && on_fade_end) + { + auto done = std::move(on_fade_end); + on_fade_end = nullptr; + run_on_ui(std::move(done)); + } + } + + void base::fade_close() + { + if (fade_closing) + return; + + if (!user_ui || !user_ui->fade_enabled || !user_ui->window_animations || !get_parent()) + { + remove_from_parent(); + return; + } + + fade_closing = true; + fade_to(0.0f, user_ui->fade_out_duration); + + // fade_to dropped any earlier callback, so this is set after it. + on_fade_end = [self = get_ptr()]() + { + if (!self->fade_closing) + return; // cancel_fade_close() came in between + + self->fade_closing = false; + self->remove_from_parent(); + self->layer_opacity = 1.0f; + }; + } + + void base::cancel_fade_close() + { + if (!fade_closing) + return; + + fade_closing = false; + fade_to(1.0f, user_ui ? float(user_ui->fade_in_duration) : 0.1f); + } + + bool base::layer_wanted() const + { + return layer_opacity < 0.999f && user_ui && user_ui->fade_enabled; + } + + void base::draw_end(Context& c) + { + draw_after(c); + + if (layer_open) + { + layer_open = false; + c.renderer->end_layer(c, c.layer_opacity); + } + } + + void base::draw_before(Context& c) + { + // The group opens first, so the blur, shadow and the element itself are + // all part of it. The shadow reaches outside the bounds, so the + // snapshot is grown by it. + layer_open = false; + + if (c.layer && c.target.resource) + { + float pad = 2.0f; + + for (auto& e : effects) + if (e->enabled) + pad = std::max(pad, e->extent(*this)); + + layer_open = c.renderer->begin_layer(c, c.layer_slot, get_render_bounds(), pad); + } + + for (auto& e : effects) + if (e->enabled) + e->draw_under(c, *this); + + draw(c); + + for (auto& e : effects) + if (e->enabled) + e->draw_over(c, *this); + } + + void BackdropBlur::collect(base& e) + { + if (e.user_ui && e.user_ui->backdrop_enabled) + slot = e.user_ui->backdrop_counter++; + } + + void BackdropBlur::draw_under(GUIInfo& c, base& e) + { + if (!active(e, c)) + return; + + BackdropBlurParams params; + params.radius = e.user_ui->backdrop_radius; + params.saturation = e.user_ui->backdrop_saturation; + params.tint.w = e.user_ui->backdrop_tint; + params.luminosity = e.user_ui->backdrop_luminosity; + params.noise = e.user_ui->backdrop_noise; + + rect r = e.get_render_bounds(); + if (r.w > 0 && r.h > 0) + c.renderer->draw_backdrop_blur(c, slot, r, params); + } + + void SlidingHighlight::update(bool present, float target_y, float target_h, float dt, bool snap) + { + const float k = snap ? 1.0f : 1.0f - std::exp(-dt * 22.0f); + + if (!present) + { + alpha += (0.0f - alpha) * k; + if (alpha < 0.003f) + alpha = 0.0f; + return; + } + + // Nothing visible yet: appear where the row is, don't fly in from the last place. + if (!valid || alpha == 0.0f) + { + y = target_y; + h = target_h; + valid = true; + } + else + { + y += (target_y - y) * k; + h += (target_h - h) * k; + } + + alpha += (1.0f - alpha) * k; + if (alpha > 0.997f) + alpha = 1.0f; + } + + float Shadow::extent(const base& e) const + { + if (!e.user_ui->shadows_enabled) + return 0.0f; + + return (3.0f * float(e.user_ui->shadow_blur) + float(e.user_ui->shadow_offset)) * (1.0f + 1.2f * lift) + 2.0f; + } + + void Shadow::update(base&, float dt) + { + if (lift == lift_target) + return; + + const float step = dt / 0.12f; + lift = lift < lift_target ? std::min(lift + step, lift_target) : std::max(lift - step, lift_target); + } + + void Shadow::draw_under(GUIInfo& c, base& e) + { + if (!e.user_ui || !e.user_ui->shadows_enabled) + return; + + ShadowStyle s = style; + s.color.w = std::min(1.0f, float(e.user_ui->shadow_opacity) * (1.0f + 0.4f * lift)); + s.blur = float(e.user_ui->shadow_blur) * (1.0f + 0.8f * lift); + s.offset = float2(0.0f, float(e.user_ui->shadow_offset) * (1.0f + 1.2f * lift)); + c.renderer->draw_shadow(c, e.get_render_bounds(), s); + } + + float Glow::extent(const base& e) const + { + return e.user_ui->glow_enabled ? 3.0f * float(e.user_ui->glow_size) + 2.0f : 0.0f; + } + + void Glow::draw_under(GUIInfo& c, base& e) + { + if (!e.user_ui || !e.user_ui->glow_enabled) + return; + + ShadowStyle g = style; + g.color.w *= e.user_ui->glow_strength; + g.blur = e.user_ui->glow_size; + g.offset = float2(0.0f, 0.0f); + c.renderer->draw_shadow(c, e.get_render_bounds(), g); + } + + void LightOverlay::draw_over(GUIInfo& c, base& e) + { + auto* ui = e.user_ui; + + if (!ui || !(reveal || border || shine || ripple || focus || inner)) + return; + + if (!(hover > 0.003f || shine_t >= 0.0f || ripple_t >= 0.0f || focus_t > 0.003f + || (inner && ui->inner_enabled))) + return; + + FxParams p; + p.radius = radius; + p.light_color = light; + const vec2 mouse = ui->get_mouse_pos(); + p.cursor = float2(mouse.x, mouse.y); + + if (reveal && ui->reveal_enabled) + { + p.reveal_strength = ui->reveal_strength * hover; + p.reveal_size = ui->reveal_size; + } + + if (border && ui->border_enabled) + { + p.border_strength = ui->border_strength * hover; + p.border_size = ui->border_size; + } + + if (shine && ui->shine_enabled && shine_t >= 0.0f) + { + p.shine_phase = shine_t; + p.shine_strength = ui->shine_strength; + } + + if (ripple && ui->ripple_enabled && ripple_t >= 0.0f) + { + p.ripple_origin = float2(ripple_origin.x, ripple_origin.y); + p.ripple_phase = ripple_t; + p.ripple_strength = ui->ripple_strength; + } + + if (focus && ui->focus_enabled) + p.focus_strength = ui->focus_strength * focus_t; + + if (inner && ui->inner_enabled) + { + p.inner_strength = ui->inner_strength; + p.inner_size = ui->inner_size; + } + + c.renderer->draw_fx(c, e.get_render_bounds(), p); + } + + void LightOverlay::update(base& e, float dt) + { + // Only the cursor-driven parts have state to advance. + if (!(reveal || border || shine || ripple || focus)) + return; + + auto* ui = e.user_ui; + + // 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 (ui) + if (auto h = ui->get_hovered().lock()) + for (const base* p = h.get(); p; p = p->parent) + if (p == &e) + { + hovered_now = true; + break; + } + + const float target = hovered_now ? 1.0f : 0.0f; + const float previous = hover; + hover += (target - hover) * (1.0f - std::exp(-dt * 14.0f)); + + if (std::abs(target - hover) < 0.003f) + hover = target; + + if (shine && ui) + { + if (previous <= 0.0f && hover > 0.0f) + shine_t = 0.0f; + else if (shine_t >= 0.0f) + { + shine_t += dt / std::max(float(ui->shine_duration), 0.05f); + + if (shine_t >= 1.0f) + shine_t = -1.0f; + } + } + + // A press starts the ripple at the cursor; it then runs out on its own. + const bool pressed_now = e.pressed; + if (ripple && ui) + { + if (pressed_now && !was_pressed) + { + ripple_t = 0.0f; + ripple_origin = ui->get_mouse_pos(); + } + else if (ripple_t >= 0.0f) + { + ripple_t += dt / std::max(float(ui->ripple_duration), 0.05f); + + if (ripple_t >= 1.0f) + ripple_t = -1.0f; + } + } + + was_pressed = pressed_now; + + if (focus) + { + const float focus_target = e.is_focused() ? 1.0f : 0.0f; + focus_t += (focus_target - focus_t) * (1.0f - std::exp(-dt * 14.0f)); + + if (std::abs(focus_target - focus_t) < 0.003f) + focus_t = focus_target; + } + } + void user_interface::process_ui(float delta_time) @@ -1143,6 +1557,8 @@ namespace GUI draw_infos.clear(); pre_draw_infos.clear(); + backdrop_counter = 0; + layer_counter = 0; draw_recursive(c); drag.draw(c); } @@ -1171,9 +1587,40 @@ namespace GUI { const uint32_t size = (uint32_t)ui_ctx.draw_infos.size(); const uint32_t per_pass = ui_per_thread(size); + + // One contiguous range per instance. A group-opacity layer copies the + // target when it opens and draws it back when it closes, so both + // have to be recorded in the same command list: a range that would + // end inside a layer runs on to the layer's end. + auto& chunks = ui_ctx.chunks; + chunks.clear(); + + const uint32_t max_chunks = Passes::UI::UI_Render::MaxCount; + uint32_t start = 0; + int depth = 0; + + while (start < size) + { + uint32_t end = chunks.size() + 1 >= max_chunks ? size : std::min(start + per_pass, size); + + for (uint32_t i = start; i < end; i++) + if (ui_ctx.draw_infos[i].layer) + depth += ui_ctx.draw_infos[i].before ? 1 : -1; + + while (depth > 0 && end < size) + { + if (ui_ctx.draw_infos[end].layer) + depth += ui_ctx.draw_infos[end].before ? 1 : -1; + end++; + } + + chunks.emplace_back(start, end); + start = end; + } + auto& ui_render_ctx = graph.get_context<::Table::UI::UIRenderState>(); ui_render_ctx.per_pass = per_pass; - ui_render_ctx.passes_needed = (size + per_pass - 1) / per_pass; + ui_render_ctx.passes_needed = (uint32_t)chunks.size(); } ui_ctx.dt = dt; @@ -1701,9 +2148,14 @@ namespace GUI mouse_move_event_internal(pos); }); + apply_cursor(); + } + + void user_interface::apply_cursor() + { ::SetCursor(LoadCursor(nullptr, cursors[static_cast(cursor)])); } - + void user_interface::mouse_action_event(mouse_action action, mouse_button button, vec2 pos) { run_on_ui([this, action, button, pos]() @@ -1809,6 +2261,8 @@ namespace GUI { back->to_front(); back->visible = true; + exit_window->layer_opacity = 0.0f; + exit_window->fade_to(1.0f, fade_in_duration); add_child(exit_window); }); return pr->get_future(); @@ -2143,10 +2597,12 @@ void PassDefault::render( auto& ui_ctx = context.graph->get_context(); - const uint32_t size = (uint32_t)ui_ctx.draw_infos.size(); - const uint32_t per_pass = context.graph->get_context<::Table::UI::UIRenderState>().per_pass; - const uint32_t start = data.pass_index * per_pass; - const uint32_t end = std::min(start + per_pass, size); + // This instance's range, chosen in create_graph so no layer is split. + if (data.pass_index >= ui_ctx.chunks.size()) + return; + + const uint32_t start = ui_ctx.chunks[data.pass_index].first; + const uint32_t end = ui_ctx.chunks[data.pass_index].second; auto command_list = context.get_list(); auto texture = (*data.swapchain); @@ -2161,10 +2617,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 +2633,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 @@ -2196,11 +2659,18 @@ void PassDefault::render( c.offset = e.offset; c.scissors = e.scissors; c.window_size = ui_ctx.scaled_size; + c.layer = e.layer; + c.layer_slot = e.layer_slot; + c.layer_opacity = e.layer_opacity; + + // The element is drawn where it was when this entry was collected, + // not where the GUI thread has since moved it. + draw_bounds_scope bounds_scope(e.elem, e.bounds); if (e.before) - e.elem->draw(c); + e.elem->draw_before(c); else - e.elem->draw_after(c); + e.elem->draw_end(c); } } diff --git a/sources/RenderSystem/GUI/Base.ixx b/sources/RenderSystem/GUI/Base.ixx index 863556636..3aead03d9 100644 --- a/sources/RenderSystem/GUI/Base.ixx +++ b/sources/RenderSystem/GUI/Base.ixx @@ -26,6 +26,17 @@ 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; + + // Group opacity of the entry being drawn, from its draw_info. + bool layer = false; + uint32_t layer_slot = 0; + float layer_opacity = 1.0f; + GUIInfo() = default; GUIInfo(HAL::CommandList::ptr list); }; @@ -76,6 +87,10 @@ struct InputHandler virtual void key_action_event(key_action action, long key, key_mods mods) = 0; virtual void char_event(char32_t ch) = 0; + + // Puts the hovered element's cursor on screen. Called from WM_SETCURSOR as well as after input, + // so the window's default arrow never gets a frame in between. + virtual void apply_cursor() {} }; namespace GUI @@ -155,9 +170,149 @@ 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 + { + 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 + bool ripple = false; // a wave from the press point + bool focus = false; // accent ring while the element has keyboard focus + bool inner = false; // darkening just inside the edge (depth) + float radius = 4.0f; // corner radius + float3 light = { 1.0f, 1.0f, 1.0f }; // sRGB colour of reveal / edge light / shine; tint it on light surfaces + }; + + // An eased highlight bar for single selection in lists and trees: it slides from + // the old row to the new one instead of jumping, fades out when nothing is + // selected, and appears in place the first time. y and h are relative to the + // scrolled content's origin, so scrolling moves the bar with its rows (easing + // them in screen space would make it lag behind). + struct SlidingHighlight + { + float y = 0.0f, h = 0.0f; + float alpha = 0.0f; // 0..1, eased + bool valid = false; + + // `snap` skips the easing (animations off). + void update(bool present, float target_y, float target_h, float dt, bool snap); + // The selected row itself moved (a tree row pushed by an expanding branch): + // the bar goes with it at once instead of trailing a few frames behind. + void follow(float target_y, float target_h) { y = target_y; h = target_h; } + }; class base; + + // One visual effect of an element: backdrop blur, shadow, glow, light overlay. + // An element holds an ordered stack of them (base::add_effect) -- most hold + // none -- and base::draw_before runs them around the element's own draw(): + // draw_under before draw(), in stack order (blur first, then shadow: the blur + // snapshot reads a little beyond the element and a shadow already + // drawn there would be blurred into the glass), + // draw() the element itself, + // draw_over after draw(), under the children (labels stay readable). + // collect() and update() run on the thread that builds the draw list, once per + // visible element per frame; draw_* run on the render threads. + // Group opacity (fades) is not an effect: it wraps the whole sequence below and + // one layer must stay inside one command list, so it stays on the element. + class effect + { + public: + bool enabled = true; + virtual ~effect() = default; + + virtual void collect(base&) {} + virtual void update(base&, float /*dt*/) {} + // How far outside the render bounds it draws, in UI px (sizes the fade layer). + virtual float extent(const base&) const { return 0.0f; } + virtual void draw_under(GUIInfo&, base&) {} + virtual void draw_over(GUIInfo&, base&) {} + }; + + // Blurs what is behind the element's render bounds. Skin shadows are drawn as + // margins outside the bounds, so the bounds are the shape. + class BackdropBlur : public effect + { + public: + // 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 standalone_only = false; + // For panels that are mostly covered by their own controls: while the + // 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; + uint32_t slot = 0; + + bool active(const base& e, const GUIInfo& c) const; + void collect(base& e) override; + void draw_under(GUIInfo& c, base& e) override; + }; + + // Soft drop shadow. Only the shape (style.radius, colour) is per element; + // strength, softness and offset come from the UI Effects settings. + class Shadow : public effect + { + public: + ShadowStyle style; + // 0..1 elevation: a lifted element (a dragged window) casts a larger, + // softer, stronger shadow. Eases toward lift_target over ~0.12 s. + float lift = 0.0f; + float lift_target = 0.0f; + + float extent(const base& e) const override; + void update(base& e, float dt) override; + void draw_under(GUIInfo& c, base& e) override; + }; + + // Soft glow around the shape (a selection ring, for instance). + class Glow : public effect + { + public: + ShadowStyle style; + + float extent(const base& e) const override; + void draw_under(GUIInfo& c, base& e) override; + }; + + // Cursor-driven light on the element's rounded rect (reveal, edge light, shine, + // ripple, focus ring, inner shadow): the flags of OverlayFx choose which. + class LightOverlay : public effect, public OverlayFx + { + public: + // hover eases 0..1 with the cursor over the element or a descendant; + // shine_t runs 0..1 after the hover starts, -1 when idle. + float hover = 0.0f; + float shine_t = -1.0f; + // ripple_t runs 0..1 after a press (-1 idle) from ripple_origin; + // focus_t eases 0..1 with keyboard focus. + float ripple_t = -1.0f; + vec2 ripple_origin = { 0, 0 }; + bool was_pressed = false; + float focus_t = 0.0f; + + void update(base& e, float dt) override; + void draw_over(GUIInfo& c, base& e) override; + }; + class drag_n_drop_package { @@ -373,6 +528,79 @@ namespace GUI virtual void draw(Context&); // override;; virtual void draw_after(Context&);; + + // The element's effect stack (see `effect`), drawn by draw_before around + // draw(). Empty for most elements. Set up when the element is built; + // it is read by the draw-list thread, so don't change it while the + // element is in the tree. + std::vector> effects; + + // The effect of type T, or null. + template + T* find_effect() const + { + for (auto& e : effects) + if (auto* t = dynamic_cast(e.get())) + return t; + return nullptr; + } + + // Appends a T to the stack (or returns the one already there) and returns it. + template + T& add_effect() + { + if (auto* t = find_effect()) + return *t; + effects.push_back(std::make_unique()); + return static_cast(*effects.back()); + } + + // Whether the element's backdrop blur is on and drawing right now. Elements + // use it to switch to a translucent look. + bool backdrop_active(const Context& c) const; + + // The parent without get_ptr(): get_parent() throws for a root that no + // shared_ptr owns (a user_interface on the stack in the GUI tests). + base* get_raw_parent() const { return parent; } + + friend class effect; + friend class BackdropBlur; + friend class Shadow; + friend class Glow; + friend class LightOverlay; + + float fade_from = 1.0f; + float fade_target = 1.0f; + float fade_t = 1.0f; // 0..1 along the fade, 1 = not fading + float fade_duration = 0.1f; + + // Group opacity: while below 1 the element and everything inside it + // is drawn as one unit and blended back with this opacity + // (Renderer::begin_layer). fade_to animates it. + float layer_opacity = 1.0f; + // Runs once, as a UI task (not inside the draw walk, which iterates + // the parent's children), when the current fade reaches its target. + std::function on_fade_end; + void fade_to(float target, float seconds); + void update_fade(float dt); + bool layer_wanted() const; + // Windows and graph nodes set this: when added they fade in as a group. + bool fade_on_add = false; + // Fades out as a group and removes the element when the fade ends (it + // stays in the tree and keeps drawing until then). Without fades it is + // removed at once. Repeated calls are harmless. + void fade_close(); + // Changed mind mid fade_close(): fade back in and stay. + void cancel_fade_close(); + bool is_fade_closing() const { return fade_closing; } + bool fade_closing = false; + bool layer_open = false; // between draw_before and draw_end + // draw_after, then closes the layer opened by draw_before. + void draw_end(Context& c); + // Background opacity for a control: the nearest ancestor's + // BackdropBlur::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); @@ -587,11 +815,41 @@ namespace GUI base* elem; vec2 offset; bool before; + // The element is a group-opacity layer: its before entry opens the + // layer, its after entry closes it. Everything in between must be + // recorded in the same command list. + bool layer = false; + // Captured with the entry: the render thread reads these, not the + // element's live state, which the GUI thread is already updating for + // the next frame. + uint32_t layer_slot = 0; + float layer_opacity = 1.0f; + // The element's render bounds when the entry was collected. Draws use + // these (see draw_bounds_scope), so geometry matches the clip above. + rect bounds; + }; + + // While alive on a draw thread, owner->get_render_bounds() returns `bounds` + // (the geometry captured when the draw list was built) instead of the live + // value. The GUI thread lays the next frame out while this one is still being + // recorded, so a moving element's live bounds run ahead of the clip rectangles + // in the draw list: its blur, shadow and fill would then be placed against + // different edges. Layout code runs on other threads and is unaffected. + class draw_bounds_scope + { + const base* previous_owner; + rect previous_bounds; + public: + draw_bounds_scope(const base* owner, const rect& bounds); + ~draw_bounds_scope(); }; struct UIContext { std::vector draw_infos; + // [begin, end) ranges of draw_infos, one per UI_Render instance, never + // splitting a layer. + std::vector> chunks; std::vector pre_draw_infos; float dt = 0; @@ -646,6 +904,61 @@ 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 }; + // 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 }; + Variable ripple_enabled = { true, "Press ripple", effects_context.get() }; + Variable ripple_strength = { 0.25f, "Ripple strength", effects_context.get(), 0.0f, 1.0f }; + Variable ripple_duration = { 0.55f, "Ripple duration (s)", effects_context.get(), 0.1f, 2.0f }; + Variable focus_enabled = { true, "Focus ring", effects_context.get() }; + Variable focus_strength = { 0.9f, "Focus ring strength", effects_context.get(), 0.0f, 1.0f }; + Variable inner_enabled = { true, "Inner shadow", effects_context.get() }; + Variable inner_strength = { 0.35f, "Inner shadow strength", effects_context.get(), 0.0f, 1.0f }; + Variable inner_size = { 10.0f, "Inner shadow size (px)", effects_context.get(), 1.0f, 48.0f }; + + // Group fades (base::layer_opacity): menu popups and message boxes fade in. + Variable fade_enabled = { true, "Group fades", effects_context.get() }; + Variable fade_in_duration = { 0.14f, "Fade-in duration (s)", effects_context.get(), 0.02f, 1.5f }; + Variable fade_out_duration = { 0.10f, "Fade-out duration (s)", effects_context.get(), 0.02f, 1.5f }; + Variable window_animations = { true, "Window open/close fades", effects_context.get() }; + + // 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; + // The same for group-opacity layers (one snapshot texture each). + uint32_t layer_counter = 0; + using ptr = s_ptr; using wptr = w_ptr; std::mutex m; @@ -675,6 +988,7 @@ namespace GUI virtual void mouse_wheel_event(mouse_wheel type, float value, vec2 pos); virtual void key_action_event(key_action action, long key, key_mods mods); virtual void char_event(char32_t ch); + virtual void apply_cursor() override; // virtual void draw_ui(Context&); diff --git a/sources/RenderSystem/GUI/Debugging/ObjectViewer.cpp b/sources/RenderSystem/GUI/Debugging/ObjectViewer.cpp index 4d6cabd46..e1c866f03 100644 --- a/sources/RenderSystem/GUI/Debugging/ObjectViewer.cpp +++ b/sources/RenderSystem/GUI/Debugging/ObjectViewer.cpp @@ -56,9 +56,9 @@ void object_tree_creator::init_element(tree_elementadd_child(l); - tree->on_toogle = [open_icon](bool v) + tree->on_toogle = [open_icon](bool expanded) { - open_icon->toogle(v); + open_icon->set_expanded(expanded); }; tree->toogle(); diff --git a/sources/RenderSystem/GUI/Elements/Button.cpp b/sources/RenderSystem/GUI/Elements/Button.cpp index b1b597c23..73898f35f 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) @@ -38,11 +35,17 @@ namespace GUI button::button() { + auto& light = add_effect(); + light.reveal = true; + light.border = true; + light.ripple = true; + light.radius = 3.0f; clickable = true; clip_child = true; label_text.reset(new label()); label_text->text = "click me!"; label_text->docking = dock::FILL; + label_text->magnet_text = FW1_CENTER | FW1_VCENTER | FW1_NOWORDWRAP; add_child(label_text); padding = {3, 3, 3, 3}; im.reset(new image()); diff --git a/sources/RenderSystem/GUI/Elements/ComboBox.cpp b/sources/RenderSystem/GUI/Elements/ComboBox.cpp index b292e8dd5..0b9fee9a2 100644 --- a/sources/RenderSystem/GUI/Elements/ComboBox.cpp +++ b/sources/RenderSystem/GUI/Elements/ComboBox.cpp @@ -95,7 +95,9 @@ bool GUI::Elements::combo_box::on_mouse_action(mouse_action action, mouse_button menu->docking = dock::NONE; menu->width_size = size_type::FIXED; - user_ui->add_child(menu); + // Fades in, or comes back if the press that reached this click + // closed it a moment ago and it is still fading out. + menu->self_open(user_ui); if (search) { @@ -109,12 +111,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()); } @@ -127,7 +126,8 @@ void GUI::Elements::combo_box::close_menus() base::close_menus(); - menu->remove_from_parent(); + // Fades out, then leaves the tree (immediately when fades are off). + menu->self_close(); } GUI::Elements::combo_box::combo_box() diff --git a/sources/RenderSystem/GUI/Elements/Container.cpp b/sources/RenderSystem/GUI/Elements/Container.cpp index 3540692ae..ff1a32174 100644 --- a/sources/RenderSystem/GUI/Elements/Container.cpp +++ b/sources/RenderSystem/GUI/Elements/Container.cpp @@ -59,6 +59,9 @@ namespace GUI Elements::container::container() { + // 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. + add_effect().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/DockBase.cpp b/sources/RenderSystem/GUI/Elements/DockBase.cpp index 785e2f308..db4c11717 100644 --- a/sources/RenderSystem/GUI/Elements/DockBase.cpp +++ b/sources/RenderSystem/GUI/Elements/DockBase.cpp @@ -41,7 +41,7 @@ bool GUI::Elements::dock_base::on_drop(drag_n_drop_package::ptr e, vec2 p) auto ui = user_ui; dock->on_empty.register_handler(w.get(), [ww, ui](bool) { - ui->add_task([ww] { ww->remove_from_parent(); }); + ui->add_task([ww] { ww->close(); }); }); } diff --git a/sources/RenderSystem/GUI/Elements/Dragger.cpp b/sources/RenderSystem/GUI/Elements/Dragger.cpp index 794acf02a..a8b6e92db 100644 --- a/sources/RenderSystem/GUI/Elements/Dragger.cpp +++ b/sources/RenderSystem/GUI/Elements/Dragger.cpp @@ -18,6 +18,8 @@ namespace GUI { dragging = action == mouse_action::DOWN; set_movable(dragging); + if (auto* shadow = real_target->find_effect()) + shadow->lift_target = dragging ? 1.0f : 0.0f; if (dragging) { diff --git a/sources/RenderSystem/GUI/Elements/EditText.cpp b/sources/RenderSystem/GUI/Elements/EditText.cpp index b548aae10..8134ca261 100644 --- a/sources/RenderSystem/GUI/Elements/EditText.cpp +++ b/sources/RenderSystem/GUI/Elements/EditText.cpp @@ -42,6 +42,13 @@ GUI::Elements::edit_text::edit_text() : edit_text(edit_style) GUI::Elements::edit_text::edit_text(Text::Style style) : style(style), editor(style) { clickable = true; + auto& light = add_effect(); + light.focus = true; + // White light vanishes on the light skin: tint the hover effects with the accent. + light.reveal = true; + light.border = true; + light.light = float3(0.21f, 0.45f, 0.79f); + light.radius = 3.0f; // Control characters never enter the text, whatever the user filter says; // a multiline field lets newlines and tabs through (typed or pasted). '\r' @@ -137,7 +144,7 @@ void GUI::Elements::edit_text::on_char(char32_t ch) { // Ctrl+letter arrives as a control character too; shortcuts are handled // from the key event instead. - if (ch < 0x20 || ch == 0x7F) return; + if (ch < 0x20 || ch == 0x7F || read_only) return; std::lock_guard guard(m); events.emplace_back(ch); } @@ -213,7 +220,7 @@ bool GUI::Elements::edit_text::on_wheel(mouse_wheel type, float value, vec2 pos) bool GUI::Elements::edit_text::need_drag_drop() { std::lock_guard guard(m); - return drag_candidate; + return drag_candidate && !read_only; } void GUI::Elements::edit_text::on_drag_start() @@ -233,7 +240,7 @@ void GUI::Elements::edit_text::generate_container(base::ptr holder) bool GUI::Elements::edit_text::can_accept(drag_n_drop_package::ptr package) { - if (!package) return false; + if (!package || read_only) return false; if (package->name == "text") return true; // Anything the image hook can turn into a texture becomes a block image. @@ -310,15 +317,15 @@ void GUI::Elements::edit_text::open_context_menu(vec2 pos) const bool selection = editor.has_selection(); - if (editor.can_undo()) add("Undo", 'Z', true); - if (editor.can_redo()) add("Redo", 'Y', true); - if (selection) add("Cut", 'X', true); - if (selection) add("Copy", 'C', true); - add("Paste", 'V', true); - if (selection) add("Delete", VK_DELETE, false); + if (!read_only && editor.can_undo()) add("Undo", 'Z', true); + if (!read_only && editor.can_redo()) add("Redo", 'Y', true); + if (!read_only && selection) add("Cut", 'X', true); + if (selection) add("Copy", 'C', true); + if (!read_only) add("Paste", 'V', true); + if (!read_only && selection) add("Delete", VK_DELETE, false); add("Select All", 'A', true); - if (selection && formatting) + if (selection && formatting && !read_only) { add("Bold", 'B', true); add("Italic", 'I', true); @@ -369,16 +376,16 @@ void GUI::Elements::edit_text::process_key(long key, key_mods mods) case VK_DOWN: editor.key(Key::Down, shift, ctrl); return; case VK_HOME: editor.key(Key::Home, shift, ctrl); return; case VK_END: editor.key(Key::End, shift, ctrl); return; - case VK_BACK: editor.key(Key::Backspace, shift, ctrl); return; - case VK_DELETE: editor.key(Key::Delete, shift, ctrl); return; + case VK_BACK: if (!read_only) editor.key(Key::Backspace, shift, ctrl); return; + case VK_DELETE: if (!read_only) editor.key(Key::Delete, shift, ctrl); return; case VK_RETURN: - if (multiline) editor.key(Key::Enter, shift, ctrl); + if (multiline && !read_only) editor.key(Key::Enter, shift, ctrl); return; case VK_TAB: // Tab arrives only as a key: on_char drops control characters. - if (multiline && !ctrl) editor.insert(U'\t'); + if (multiline && !ctrl && !read_only) editor.insert(U'\t'); return; } @@ -391,11 +398,11 @@ void GUI::Elements::edit_text::process_key(long key, key_mods mods) break; case 'B': - if (formatting) editor.toggle_bold(); + if (formatting && !read_only) editor.toggle_bold(); break; case 'I': - if (formatting) editor.toggle_italic(); + if (formatting && !read_only) editor.toggle_italic(); break; case 'C': @@ -404,20 +411,21 @@ void GUI::Elements::edit_text::process_key(long key, key_mods mods) case 'X': copy_selection(); - editor.delete_selection(); + if (!read_only) editor.delete_selection(); break; case 'V': - if (user_ui && user_ui->get_clipboard) + if (!read_only && user_ui && user_ui->get_clipboard) editor.insert(utf16_to_utf8(user_ui->get_clipboard())); break; case 'Z': + if (read_only) break; if (shift) editor.redo(); else editor.undo(); break; case 'Y': - editor.redo(); + if (!read_only) editor.redo(); break; } } @@ -670,7 +678,7 @@ void GUI::Elements::edit_text::on_pre_render(Context& c) text_dirty = true; } - caret_visible = is_focused(); + const bool focused = is_focused(); // Wide enough for the largest number (at least two digits, so it doesn't // jump at line 10), plus a gap on each side. Before the scroll update: @@ -691,7 +699,20 @@ void GUI::Elements::edit_text::on_pre_render(Context& c) const rect content = content_rect(c); const vec2 view = vec2(content.w, content.h) / c.scale; drag_select_scroll(c, content, view, scroll_before); + const Text::Caret prev_caret = caret; caret = editor.caret(); + + // Blink like a native field: solid right after focus or any caret move/edit, so + // the caret never vanishes while typing or arrowing, then on/off at a steady rate. + constexpr float blink_period = 1.06f; + const bool moved = caret.center.x != prev_caret.center.x || caret.center.y != prev_caret.center.y + || caret.height != prev_caret.height; + if (!focused || !was_focused || moved) + caret_blink_time = 0.0f; + else + caret_blink_time = std::fmod(caret_blink_time + c.delta_time, blink_period); + was_focused = focused; + caret_visible = focused && caret_blink_time < blink_period * 0.5f; update_scroll(view); if (multiline) sync_scroll_bars(view, editor.content_size() + vec2(4, 0)); diff --git a/sources/RenderSystem/GUI/Elements/EditText.ixx b/sources/RenderSystem/GUI/Elements/EditText.ixx index e8e89bc8d..9919d9270 100644 --- a/sources/RenderSystem/GUI/Elements/EditText.ixx +++ b/sources/RenderSystem/GUI/Elements/EditText.ixx @@ -124,6 +124,8 @@ export namespace GUI std::vector selection; Text::Caret caret; bool caret_visible = false; + float caret_blink_time = 0.0f; // seconds into the blink cycle + bool was_focused = false; // Block images: the document holds only ids (Text::Editor image // paragraphs), the textures live here. Guarded by m. @@ -163,6 +165,11 @@ export namespace GUI // survive paste. Single-line fields drop them. bool multiline = false; + // Selecting, copying and scrolling work; anything that changes the text + // (typing, paste, cut, delete, undo, formatting, drops) is ignored. + // set_text() still replaces the content. + bool read_only = false; + // A gutter with 1-based line numbers left of the text (multiline). bool line_numbers = false; diff --git a/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp b/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp index f41dcc4a6..3d8779e41 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/Canvas.cpp @@ -65,14 +65,18 @@ namespace class toolbar_panel : public GUI::base { public: - void draw(GUI::base::Context& c) override + toolbar_panel() { - GUI::Texture shadow = Skin::get().Shadow; - shadow.margins = { 12, 12, 12, 12 }; - c.renderer->draw(c, shadow, get_render_bounds()); + // The buttons cover all but the 4 px padding, so they go glass too. + add_effect().glass_children = true; + add_effect(); + } + void draw(GUI::base::Context& c) override + { + // 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()); } }; @@ -190,7 +194,9 @@ void GUI::Elements::FlowGraph::canvas::draw(Context& c) g->cam_pos = contents->pos; c.renderer->flush(c); auto clip = c.scissors; - c.command_list->get_graphics().set_scissors(c.ui_clipping); + // ui_clipping alone ignores scissors set by ancestors (self/child scissor), so the + // splines would spill out of them; narrow the active scissor instead of replacing it. + c.command_list->get_graphics().set_scissors(sizer_long(intersect(sizer(c.scissors), sizer(c.ui_clipping)))); Slots::UI::FlowGraph graph_data; graph_data.GetSize() = vec4(render_bounds->size, user_ui->size.get()); @@ -302,7 +308,7 @@ void GUI::Elements::FlowGraph::canvas::on_remove(::FlowGraph::window* w) run_on_ui([this, node]() { auto elem = nodes[node]; - elem->remove_from_parent(); + elem->fade_close(); nodes.erase(node); }); } @@ -315,7 +321,7 @@ void GUI::Elements::FlowGraph::canvas::on_remove(::FlowGraph::window* w) run_on_ui([this, w]() { auto elem = windows[w]; - elem->remove_from_parent(); + elem->fade_close(); windows.erase(w); }); } diff --git a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp index 30cdaebf3..87e29bf8e 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.cpp @@ -65,9 +65,22 @@ 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; + fade_on_add = true; + add_effect(); + add_effect(); + // Lit while the node is selected (on_pre_render). + glow = &add_effect(); + glow->enabled = false; + glow->style.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..0624e0806 100644 --- a/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.ixx +++ b/sources/RenderSystem/GUI/Elements/FlowGraph/ComponentWindow.ixx @@ -61,6 +61,7 @@ export namespace GUI window_type type; canvas* canva; Skin::Window skin; + GUI::Glow* glow = nullptr; // in the effect stack; lit while selected public: @@ -68,6 +69,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/Label.cpp b/sources/RenderSystem/GUI/Elements/Label.cpp index c0c762b87..9330354dd 100644 --- a/sources/RenderSystem/GUI/Elements/Label.cpp +++ b/sources/RenderSystem/GUI/Elements/Label.cpp @@ -160,6 +160,19 @@ namespace GUI // Text is issued directly, not through the batched NinePatch path, so // anything queued ahead of it must reach the list first. c.renderer->flush(c); + + // Two offset passes, the far one fainter, so the shadow fades out instead of + // reading as a hard duplicate of the glyphs. + if (shadow_color.w > 0.0f) + { + const vec2 step = shadow_offset * c.scale; + // Clipped to the parent, not the label box: the shadow falls outside the glyphs' box. + const sizer shadow_clip = sizer(c.ui_clipping); + Text::Engine::get().draw(c.command_list, layout, pos + step * 2.0f, + float4(shadow_color.x, shadow_color.y, shadow_color.z, shadow_color.w * 0.35f), shadow_clip, c.window_size); + Text::Engine::get().draw(c.command_list, layout, pos + step, shadow_color, shadow_clip, c.window_size); + } + Text::Engine::get().draw(c.command_list, layout, pos, color, clip, c.window_size); } diff --git a/sources/RenderSystem/GUI/Elements/Label.ixx b/sources/RenderSystem/GUI/Elements/Label.ixx index 475def007..d5e1aeb4e 100644 --- a/sources/RenderSystem/GUI/Elements/Label.ixx +++ b/sources/RenderSystem/GUI/Elements/Label.ixx @@ -66,6 +66,11 @@ export namespace GUI property text; property font_size; float4 color; + + // Drop shadow behind the text; off while the alpha is 0. The offset is in + // layout units, so it scales with the UI. + float4 shadow_color = float4(0, 0, 0, 0); + vec2 shadow_offset = vec2(1, 1); label(); virtual ~label(); virtual void draw(Context& c) override; diff --git a/sources/RenderSystem/GUI/Elements/ListBox.cpp b/sources/RenderSystem/GUI/Elements/ListBox.cpp index d14bc5fb6..a67879004 100644 --- a/sources/RenderSystem/GUI/Elements/ListBox.cpp +++ b/sources/RenderSystem/GUI/Elements/ListBox.cpp @@ -10,9 +10,79 @@ import ; } +void GUI::Elements::list_box::on_pre_render(Context& c) +{ + scroll_container::on_pre_render(c); + + list_element* selected = nullptr; + if (!multi_select) + for (auto& e : elements) + if (e->selected) + { + selected = e.get(); + break; + } + + float y = 0.0f, h = 0.0f; + if (selected) + { + y = selected->get_render_bounds().y - contents->get_render_bounds().y; + h = selected->get_render_bounds().h; + } + + const bool animate = user_ui && user_ui->window_animations; + bar.update(selected != nullptr, y, h, c.delta_time, !animate); + + // The row under the cursor: the hit test finds a row's label, so look up the parents. + list_element* hovered = nullptr; + if (user_ui) + if (auto hit = user_ui->get_hovered().lock()) + for (base* p = hit.get(); p && !hovered; p = p->get_raw_parent()) + for (auto& e : elements) + if (e.get() == p) + { + hovered = e.get(); + break; + } + + float hover_y = 0.0f, hover_h = 0.0f; + if (hovered) + { + hover_y = hovered->get_render_bounds().y - contents->get_render_bounds().y; + hover_h = hovered->get_render_bounds().h; + } + hover_bar.update(hovered != nullptr, hover_y, hover_h, c.delta_time, !animate); +} + void GUI::Elements::list_box::draw(Context& c) { c.renderer->draw_area(get_ptr(), c); + + if (elements.empty()) + return; + + // Over the background, under the rows (their text), inside the box. + const rect box = get_render_bounds(); + const rect row = elements.front()->get_render_bounds(); + const float content_y = contents->get_render_bounds().y; + + auto draw_bar = [&](const SlidingHighlight& b, float alpha_scale) + { + if (b.alpha <= 0.0f) + return; + + const float top = std::max(content_y + b.y, box.y + padding->top * result_scale); + const float bottom = std::min(content_y + b.y + b.h, box.y + box.h - padding->bottom * result_scale); + if (bottom <= top) + return; + + float4 color = float4(53, 114, 202, 255) / 255.0f; + color.w *= b.alpha * alpha_scale; + c.renderer->draw_color(c, color, rect(row.x, top, row.w, bottom - top)); + }; + + draw_bar(hover_bar, 0.22f); // under the selection, which stays the strong one + draw_bar(bar, 1.0f); } GUI::Elements::list_element::ptr GUI::Elements::list_box::add_item(std::string elem) @@ -98,10 +168,29 @@ namespace GUI { + void Elements::list_element::on_pre_render(Context& c) + { + base::on_pre_render(c); + + const float target = selected ? 1.0f : 0.0f; + const bool animate = user_ui && user_ui->window_animations; + + if (!animate) + selection_t = target; + else + selection_t = selection_t < target ? std::min(selection_t + c.delta_time / 0.12f, target) + : std::max(selection_t - c.delta_time / 0.12f, target); + } + void Elements::list_element::draw(Context& c) { - if (selected) - c.renderer->draw_color(c, float4(53, 114, 202, 255) / 255.0f, get_render_bounds()); + // Single selection: the list's sliding bar draws it. + if (selection_t > 0.0f && owner && owner->multi_select) + { + float4 color = float4(53, 114, 202, 255) / 255.0f; + color.w *= selection_t; + c.renderer->draw_color(c, color, get_render_bounds()); + } } bool Elements::list_element::on_mouse_action(mouse_action action, mouse_button button, vec2 pos) diff --git a/sources/RenderSystem/GUI/Elements/ListBox.ixx b/sources/RenderSystem/GUI/Elements/ListBox.ixx index c8b4caa70..0424a0102 100644 --- a/sources/RenderSystem/GUI/Elements/ListBox.ixx +++ b/sources/RenderSystem/GUI/Elements/ListBox.ixx @@ -20,8 +20,11 @@ export namespace GUI bool selected = false; list_box* owner; + // The highlight eases in and out (0..1) instead of switching. + float selection_t = 0.0f; list_element(list_box* owner); + virtual void on_pre_render(Context& c) override; virtual void draw(Context& c) override; virtual bool on_mouse_action(mouse_action action, mouse_button button, vec2 pos) override; @@ -35,6 +38,11 @@ export namespace GUI std::vector elements; int anchor = -1; // where a shift-click range starts + // Single selection: one bar slides to the selected row (multi-select + // fades each row's own highlight instead). + SlidingHighlight bar; + // The row under the cursor: a fainter bar that slides between rows too. + SlidingHighlight hover_bar; public: @@ -47,6 +55,10 @@ export namespace GUI std::vector selected_indices() const; + // The sliding selection bar (single selection), for inspection. + const SlidingHighlight& highlight() const { return bar; } + const SlidingHighlight& hover_highlight() const { return hover_bar; } + void clear_items(); // class renderer; //friend class renderer; @@ -57,6 +69,7 @@ export namespace GUI list_box(); list_element::ptr add_item(std::string elem); + virtual void on_pre_render(Context& c) override; virtual void draw(Context& c) override; void on_select(list_element*, bool shift = false, bool ctrl = false); diff --git a/sources/RenderSystem/GUI/Elements/MenuList.cpp b/sources/RenderSystem/GUI/Elements/MenuList.cpp index 61fb56f3d..d8af76e96 100644 --- a/sources/RenderSystem/GUI/Elements/MenuList.cpp +++ b/sources/RenderSystem/GUI/Elements/MenuList.cpp @@ -5,12 +5,26 @@ 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 and sit slightly recessed; the menu strip is a flat bar. + if (vertical) + { + add_effect(); + add_effect(); + auto& light = add_effect(); + light.inner = true; + light.radius = 0.0f; + } padding = { 1, 1, 1, 1 }; docking = GUI::dock::NONE; if (!vertical) @@ -99,12 +113,50 @@ bool GUI::Elements::menu_list::is_self_open() void GUI::Elements::menu_list::self_open(user_interface* ui) { + // Closed a moment ago and still fading out: bring the same popup back + // instead of adding it a second time. + if (get_parent()) + { + cancel_close(); + return; + } + ui->add_child(get_ptr()); + + // Every popup (submenus, context menus, dropdowns) fades in as one group: + // the blur, the shadow and the items together. + layer_opacity = 0.0f; + fade_to(1.0f, ui->fade_in_duration); } void GUI::Elements::menu_list::self_close() { - remove_from_parent(); + // Without fades (or a tree to draw it in) the popup just goes. + if (!user_ui || !user_ui->fade_enabled || !get_parent()) + { + remove_from_parent(); + return; + } + + if (fading_out) + return; + + fading_out = true; + fade_to(0.0f, user_ui->fade_out_duration); + + // fade_to dropped any earlier callback; set ours after it. + on_fade_end = [self = get_ptr()]() + { + self->fading_out = false; + self->remove_from_parent(); + self->layer_opacity = 1.0f; + }; +} + +void GUI::Elements::menu_list::cancel_close() +{ + fading_out = false; + fade_to(1.0f, user_ui ? float(user_ui->fade_in_duration) : 0.1f); } void GUI::Elements::menu_list::remove_all() @@ -124,6 +176,11 @@ void GUI::Elements::menu_list::remove_all() GUI::Elements::menu_list_element::menu_list_element(bool on_strip) { + auto& light = add_effect(); + light.reveal = true; + light.border = true; + light.ripple = true; + light.radius = 0.0f; this->on_strip = on_strip; } @@ -207,7 +264,8 @@ namespace GUI { - user_ui->add_child(menu); + // Fades in, or comes back if it is still fading out from a close. + menu->self_open(user_ui); rect p = get_render_bounds(); @@ -227,7 +285,8 @@ namespace GUI { menu->close_menus(); - menu->remove_from_parent(); + // Fades out, then leaves the tree (immediately when fades are off). + menu->self_close(); } diff --git a/sources/RenderSystem/GUI/Elements/MenuList.ixx b/sources/RenderSystem/GUI/Elements/MenuList.ixx index 758b2ab9d..952280c45 100644 --- a/sources/RenderSystem/GUI/Elements/MenuList.ixx +++ b/sources/RenderSystem/GUI/Elements/MenuList.ixx @@ -52,6 +52,10 @@ export namespace GUI { std::vector elements; + + // Fading out after a close; the popup leaves the tree when the + // fade ends (on_fade_end), not before. + bool fading_out = false; protected: bool vertical; @@ -142,6 +146,9 @@ export namespace GUI void self_close(); + // Brings a popup that is still fading out after a close back in. + void cancel_close(); + virtual void remove_all(); virtual void close_menus() override; diff --git a/sources/RenderSystem/GUI/Elements/ProgressBar.cpp b/sources/RenderSystem/GUI/Elements/ProgressBar.cpp index df6270362..8cac70e23 100644 --- a/sources/RenderSystem/GUI/Elements/ProgressBar.cpp +++ b/sources/RenderSystem/GUI/Elements/ProgressBar.cpp @@ -14,6 +14,29 @@ namespace GUI size = {0, 8}; } + void progress_bar::on_pre_render(Context& c) + { + base::on_pre_render(c); + + const float raw = value; + const float target = std::clamp(raw, 0.0f, 1.0f); + + if (!animated || first) + { + shown = target; + first = false; + } + else + { + // Exponential ease: about 90% of a jump in 0.15 s, whatever the frame time. + shown += (target - shown) * (1.0f - std::exp(-c.delta_time * 15.0f)); + if (std::abs(target - shown) < 0.0005f) + shown = target; + } + + sweep = std::fmod(sweep + c.delta_time / 1.6f, 1.0f); + } + void progress_bar::draw(Context& c) { rect b = get_render_bounds(); @@ -21,14 +44,57 @@ namespace GUI // background track c.renderer->draw_color(c, track_color, b); - // filled portion — clamp value to [0,1] - float raw = value; - float v = raw < 0.0f ? 0.0f : (raw > 1.0f ? 1.0f : raw); - if (v > 0.0f) + // filled portion, eased value in [0,1] + const float v = animated ? shown : std::clamp(float(value), 0.0f, 1.0f); + if (v <= 0.0f) + return; + + rect fill = b; + fill.size.x *= v; + c.renderer->draw_color(c, fill_color, fill); + + if (!animated) + return; + + // Highlight band sweeping left to right over the fill, a quarter of its + // width (at least 24 px), faded at both ends by stacking slices. + // Thin slices (about 2 px) so the falloff reads as a gradient, not steps. + const float band = std::max(24.0f, fill.size.x * 0.25f); + const float x0 = fill.pos.x - band + (fill.size.x + band) * sweep; + const int slices = std::clamp(int(band / 2.0f), 12, 48); + for (int i = 0; i < slices; i++) + { + const float t = (i + 0.5f) / slices; // 0..1 across the band + const float wave = std::sin(t * 3.14159265f); + const float alpha = 0.28f * wave * wave; // soft at both ends + float sx = x0 + band * i / slices; + float ex = x0 + band * (i + 1) / slices; + sx = std::max(sx, fill.pos.x); + ex = std::min(ex, fill.pos.x + fill.size.x); + if (ex <= sx) + continue; + + rect s = fill; + s.pos.x = sx; + s.size.x = ex - sx; + c.renderer->draw_color(c, float4(1, 1, 1, alpha), s); + } + + // Leading-edge glow: a short falloff just outside the fill, in its colour. + if (v < 0.999f) { - rect fill = b; - fill.size.x *= v; - c.renderer->draw_color(c, fill_color, fill); + const float edge = fill.pos.x + fill.size.x; + const float length = std::min(b.size.y * 2.0f, b.pos.x + b.size.x - edge); + const int steps = std::clamp(int(length / 1.5f), 1, 24); + for (int i = 0; i < steps && length > 0.0f; i++) + { + const float t = (i + 0.5f) / steps; + const float alpha = 0.35f * (1.0f - t) * (1.0f - t); + rect g = b; + g.pos.x = edge + length * i / steps; + g.size.x = length / steps; + c.renderer->draw_color(c, float4(fill_color.x, fill_color.y, fill_color.z, alpha), g); + } } } } diff --git a/sources/RenderSystem/GUI/Elements/ProgressBar.ixx b/sources/RenderSystem/GUI/Elements/ProgressBar.ixx index d1b03d345..acdfdf68f 100644 --- a/sources/RenderSystem/GUI/Elements/ProgressBar.ixx +++ b/sources/RenderSystem/GUI/Elements/ProgressBar.ixx @@ -17,9 +17,21 @@ export namespace GUI Events::Event on_change; + // Animated look: the fill eases toward value, a soft highlight sweeps + // along it, and the leading edge glows. Off = a plain flat bar. + bool animated = true; + // The eased fill actually drawn, 0..1. + float displayed() const { return shown; } + progress_bar(); + virtual void on_pre_render(Context& c) override; virtual void draw(Context& c) override; + + private: + float shown = 0.0f; // eased value actually drawn + float sweep = 0.0f; // 0..1 position of the highlight, wraps + bool first = true; // the first frame snaps instead of easing from 0 }; } } diff --git a/sources/RenderSystem/GUI/Elements/StatGraph.cpp b/sources/RenderSystem/GUI/Elements/StatGraph.cpp index a3d20d148..fc13e84d0 100644 --- a/sources/RenderSystem/GUI/Elements/StatGraph.cpp +++ b/sources/RenderSystem/GUI/Elements/StatGraph.cpp @@ -32,37 +32,30 @@ namespace GUI width_size = size_type::MATCH_PARENT; size = {0, 60}; - header_label.reset(new label()); - header_label->docking = dock::NONE; - header_label->x_type = pos_x_type::LEFT; - header_label->y_type = pos_y_type::TOP; - header_label->pos = {4, 2}; - header_label->width_size = size_type::FIXED; - header_label->height_size = size_type::FIXED; - header_label->size = {0, 14}; - header_label->magnet_text = FW1_LEFT | FW1_VCENTER | FW1_NOWORDWRAP; - header_label->color = {0.9f, 0.9f, 0.9f, 0.85f}; - add_child(header_label); - - auto make_legend_label = [this]() -> label::ptr { + // Labels size themselves to their text (label::on_text_changed); layout_labels() + // then widens them to their slot and positions them, so nothing is left to the + // dock/size machinery that used to flip between text width and graph width. + auto make_label = [this](float font, float4 color) -> label::ptr { auto l = label::ptr(new label()); l->docking = dock::NONE; l->x_type = pos_x_type::LEFT; l->y_type = pos_y_type::TOP; - l->pos = {0, 2}; l->width_size = size_type::FIXED; l->height_size = size_type::FIXED; - l->size = {0, 12}; - l->magnet_text = FW1_RIGHT | FW1_VCENTER | FW1_NOWORDWRAP; - l->color = {0.75f, 0.75f, 0.75f, 0.65f}; + l->magnet_text = FW1_LEFT | FW1_TOP | FW1_NOWORDWRAP; + l->font_size = font; + l->color = color; + l->shadow_color = {0.0f, 0.0f, 0.0f, 0.85f}; + add_child(l); return l; }; - _max_label = make_legend_label(); - add_child(_max_label); - - _min_label = make_legend_label(); - add_child(_min_label); + // Colours follow line_color and are applied in layout_labels. + header_label = make_label(15.0f, line_color); + _cur.lbl = make_label(15.0f, line_color); + _avg.lbl = make_label(13.0f, line_color); + _max.lbl = make_label(13.0f, line_color); + _min.lbl = make_label(13.0f, line_color); draw_tex.mul_color = {1, 1, 1, 1}; draw_tex.add_color = {0, 0, 0, 0}; @@ -176,7 +169,7 @@ namespace GUI } } - update_label(); + update_label(dt); } float stat_graph::current_value() const @@ -209,54 +202,96 @@ namespace GUI return m; } - void stat_graph::update_label() + void stat_graph::update_label(float dt) { - header_label->visible = show_stats; - _max_label->visible = show_stats; - _min_label->visible = show_stats; + for (label::ptr l : {header_label, _cur.lbl, _avg.lbl, _max.lbl, _min.lbl}) + l->visible = show_stats; if (!show_stats) return; - char buf[128]; - float cur = current_value(); - float avg = average_value(); + // The texts are rewritten a few times a second, not per frame: a value that + // changes every frame is unreadable, and every rewrite re-lays-out the glyphs. + constexpr float refresh_interval = 0.25f; + _label_accum += dt; + if (dt > 0.0f && _label_accum < refresh_interval) return; + _label_accum = 0.0f; + + const std::string u = unit.empty() ? "" : " " + unit; + auto fmt = [&](const char* prefix, float v) { + char buf[64]; + snprintf(buf, sizeof(buf), "%s%.1f%s", prefix, v, u.c_str()); + return std::string(buf); + }; - const char* u = unit.empty() ? "" : unit.c_str(); - const char* usp = unit.empty() ? "" : " "; + // Setting the text makes the label size itself to it; the slot only grows. + auto set = [](readout& r, const std::string& s) { + r.lbl->text = s; + r.slot_w = std::max(r.slot_w, vec2(r.lbl->size).x); + }; - if (title.empty()) - snprintf(buf, sizeof(buf), "%.1f%s%s avg %.1f%s%s", cur, usp, u, avg, usp, u); - else - snprintf(buf, sizeof(buf), "%s: %.1f%s%s avg %.1f%s%s", - title.c_str(), cur, usp, u, avg, usp, u); + header_label->text = title; + set(_cur, fmt("", current_value())); + set(_avg, fmt("avg ", average_value())); + // Actual data min/max, not the padded display range. + set(_min, fmt("min ", min_stored())); + set(_max, fmt("max ", max_stored())); + } - header_label->text = buf; + void stat_graph::layout_labels(float w, float h) + { + if (!show_stats) return; + + constexpr float pad = 4.0f, gap = 10.0f; + auto height = [](const label::ptr& l) { return vec2(l->size).y; }; - // Legend: actual data min/max (not the padded display range) - snprintf(buf, sizeof(buf), "%.1f%s%s ", max_stored(), usp, u); - _max_label->text = buf; + // Slot widths come from the ratchet, never from the current text, so the + // positions below are stable between refreshes. + auto place = [&](const label::ptr& l, float slot_w, float x, float row_y, float row_h) { + const float lh = height(l); + l->size = {std::max(slot_w, vec2(l->size).x), lh}; + l->pos = {x, row_y + (row_h - lh) * 0.5f}; + }; - snprintf(buf, sizeof(buf), "%.1f%s%s ", min_stored(), usp, u); - _min_label->text = buf; + // Everything lives in one bottom row, all in the graph's own colour: + // "title value" on the left, "avg min max" on the right. + const float3 rgb = {line_color.x, line_color.y, line_color.z}; + header_label->color = {rgb.x, rgb.y, rgb.z, 0.85f}; + _cur.lbl->color = {rgb.x, rgb.y, rgb.z, 1.0f}; + for (readout* r : {&_avg, &_min, &_max}) + r->lbl->color = {rgb.x, rgb.y, rgb.z, 0.85f}; + + const float title_w = vec2(header_label->size).x; + const float row_h = std::max({height(header_label), height(_cur.lbl), height(_avg.lbl), + height(_min.lbl), height(_max.lbl)}); + const float row_y = h - row_h - 2.0f; + + place(header_label, title_w, pad, row_y, row_h); + const float cur_x = pad + title_w + 6.0f; + place(_cur.lbl, _cur.slot_w, cur_x, row_y, row_h); + + // Right to left, max first; a readout that would run into the left group + // (or one already dropped) is hidden instead of overlapping. + const float left_limit = cur_x + _cur.slot_w + gap; + float x = w - pad; + bool fits = true; + for (readout* r : {&_max, &_min, &_avg}) + { + x -= r->slot_w; + fits = fits && x >= left_limit; + r->lbl->visible = fits; + if (fits) + place(r->lbl, r->slot_w, x, row_y, row_h); + x -= gap; + } } // ── GPU rendering hooks ──────────────────────────────────────────────── void stat_graph::on_pre_render(Context& c) { - // Keep min label pinned to the bottom edge auto bounds = get_render_bounds(); // Label positions are in layout units; the bounds are pixels. const float layout_scale = result_scale * scale; - const float h = bounds.h / layout_scale; - if (h > 14.0f) - _min_label->pos = {0, h - 14.0f}; - - // MATCH_PARENT would copy the parent's pixel width into layout units, overshooting by the - // scale factor (and right-aligned text would land outside the graph). - const float w = bounds.w / layout_scale; - header_label->size = {w, 14}; - _max_label->size = {w, 12}; - _min_label->size = {w, 12}; + layout_labels(bounds.w / layout_scale, bounds.h / layout_scale); if (count == 0) return; if (bounds.w < 4.0f || bounds.h < 4.0f) return; diff --git a/sources/RenderSystem/GUI/Elements/StatGraph.ixx b/sources/RenderSystem/GUI/Elements/StatGraph.ixx index 699024a65..bc0c30f24 100644 --- a/sources/RenderSystem/GUI/Elements/StatGraph.ixx +++ b/sources/RenderSystem/GUI/Elements/StatGraph.ixx @@ -25,10 +25,18 @@ export namespace GUI float _smooth_max = 0.0f; bool _range_valid = false; - label::ptr header_label; - label::ptr _max_label; - label::ptr _min_label; - void update_label(); + // Each readout is its own label in a slot that only ever grows, so a + // digit-count change cannot move its neighbours or resize its box. + struct readout + { + label::ptr lbl; + float slot_w = 0.0f; + }; + label::ptr header_label; // title, left + readout _cur, _avg, _max, _min; + float _label_accum = 1.0f; // seconds since the texts were last rewritten + void update_label(float dt); + void layout_labels(float w, float h); float get_sample(size_t i) const; void compute_auto_range(float& vmin, float& vmax) const; diff --git a/sources/RenderSystem/GUI/Elements/TabControl.cpp b/sources/RenderSystem/GUI/Elements/TabControl.cpp index df1f2dab5..89402a854 100644 --- a/sources/RenderSystem/GUI/Elements/TabControl.cpp +++ b/sources/RenderSystem/GUI/Elements/TabControl.cpp @@ -147,14 +147,52 @@ namespace GUI // context.renderer->draw(c, skin.Normal, get_render_bounds()); c.renderer->draw_container(get_ptr(),c); } + void tab_strip::on_pre_render(Context& c) + { + base::on_pre_render(c); + + if (!current || !current->visible.get()) + { + indicator_valid = false; + return; + } + + const rect cb = current->get_render_bounds(); + if (!indicator_valid) + { + indicator_x = cb.x; + indicator_w = cb.w; + indicator_valid = true; + return; + } + + // Exponential ease, frame-rate independent: about 90% of a jump in 0.1 s. + const float k = 1.0f - std::exp(-c.delta_time * 23.0f); + indicator_x += (cb.x - indicator_x) * k; + indicator_w += (cb.w - indicator_w) * k; + } + void tab_strip::draw_after(Context& c) { - // context.renderer->draw(c, skin.Normal, get_render_bounds()); - // context.renderer->draw_container(get_ptr(),c); + const float4 accent = float4(53, 114, 202, 255) / 255.0f; auto bounds = get_render_bounds(); bounds.y += bounds.h - 3; bounds.h = 3; - c.renderer->draw_color(c, float4(53, 114, 202,255)/255.0f, bounds); + + if (!indicator_valid) + { + c.renderer->draw_color(c, accent, bounds); + return; + } + + // A faint baseline under the whole strip, the accent under the current tab only. + c.renderer->draw_color(c, float4(accent.x, accent.y, accent.z, 0.25f), bounds); + + rect ind = bounds; + ind.x = std::max(indicator_x, bounds.x); + ind.w = std::min(indicator_x + indicator_w, bounds.x + bounds.w) - ind.x; + if (ind.w > 0) + c.renderer->draw_color(c, accent, ind); } tab_strip::tab_strip() diff --git a/sources/RenderSystem/GUI/Elements/TabControl.ixx b/sources/RenderSystem/GUI/Elements/TabControl.ixx index 3a3868551..cb3b8e159 100644 --- a/sources/RenderSystem/GUI/Elements/TabControl.ixx +++ b/sources/RenderSystem/GUI/Elements/TabControl.ixx @@ -62,6 +62,14 @@ export namespace GUI virtual void draw(Context& c) override; virtual void draw_after(Context&) override; + virtual void on_pre_render(Context& c) override; + + private: + // The accent underline slides to the current tab (x and width, in render px). + float indicator_x = 0.0f, indicator_w = 0.0f; + bool indicator_valid = false; + + public: virtual void on_bounds_changed(const rect& r) override; virtual void close_menus() override; diff --git a/sources/RenderSystem/GUI/Elements/Toast.cpp b/sources/RenderSystem/GUI/Elements/Toast.cpp index ae8bb855d..f2349b94a 100644 --- a/sources/RenderSystem/GUI/Elements/Toast.cpp +++ b/sources/RenderSystem/GUI/Elements/Toast.cpp @@ -14,6 +14,8 @@ namespace std::vector pending; constexpr float fade_time = 0.4f; + constexpr float slide_in = 36.0f; // px, the distance it arrives from + constexpr float slide_out = 24.0f; // px, the distance it leaves to } namespace GUI @@ -37,6 +39,9 @@ namespace GUI docking = dock::TOP; height_size = size_type::MATCH_CHILDREN; margin = { 0, 6, 0, 0 }; + // Frosted: the dark background below is translucent, so the blur of + // what is behind the toast shows through it. + add_effect(); // No padding: PARENT-docked children are placed inside it, and the // background and strip must reach the edges. The text's offset // comes from its own margins instead. @@ -76,9 +81,30 @@ namespace GUI void toast_widget::set_opacity(float a) { - background->color = float4(0.13f, 0.13f, 0.14f, 0.94f * a); - strip->color = float4(accent.x, accent.y, accent.z, a); - message->color = float4(1, 1, 1, a); + // As a group the blur, background, strip and text fade together; + // per-element alpha would let the strip show through the text + // area and leave the blur at full strength. + const bool group = user_ui && user_ui->fade_enabled; + layer_opacity = group ? a : 1.0f; + const float alpha = group ? 1.0f : a; + + background->color = float4(0.09f, 0.10f, 0.13f, 0.52f * alpha); + strip->color = float4(accent.x, accent.y, accent.z, alpha); + message->color = float4(1, 1, 1, alpha); + } + + void toast_widget::set_offset(float dx) + { + if (std::abs(dx - offset) < 0.05f) + return; + + offset = dx; + // Shifted by margins: the right one gives back what the left one + // takes, so the width stays the same and only the position moves. + margin = { dx, 6, -dx, 0 }; + // A margin change only flags the parent; without this the toast moves + // but its children keep the positions of the last full layout. + set_update_layout(); } bool toast_widget::on_mouse_action(mouse_action action, mouse_button button, vec2 pos) @@ -123,6 +149,8 @@ namespace GUI stack->height_size = size_type::MATCH_CHILDREN; stack->size = { 360, 0 }; stack->margin = { 0, 0, 16, 16 }; + // The toasts slide in from beyond its right edge. + stack->clip_child = false; ui->add_child(stack); } } @@ -154,7 +182,14 @@ namespace GUI { t->remaining -= dt; const float age = t->lifetime - t->remaining; - t->set_opacity(std::clamp(std::min(age, t->remaining) / fade_time, 0.0f, 1.0f)); + const float in = std::clamp(age / fade_time, 0.0f, 1.0f); + const float out = std::clamp(t->remaining / fade_time, 0.0f, 1.0f); + auto smooth = [](float x) { return x * x * (3.0f - 2.0f * x); }; + + t->set_opacity(smooth(std::min(in, out))); + + // Slides in from the right, and a little way back out when it leaves. + t->set_offset(slide_in * (1.0f - smooth(in)) + slide_out * (1.0f - smooth(out))); } // Removed from the UI thread's queue rather than here: think() runs diff --git a/sources/RenderSystem/GUI/Elements/Toast.ixx b/sources/RenderSystem/GUI/Elements/Toast.ixx index dbeca45f5..776709ee0 100644 --- a/sources/RenderSystem/GUI/Elements/Toast.ixx +++ b/sources/RenderSystem/GUI/Elements/Toast.ixx @@ -31,9 +31,20 @@ export namespace GUI toast_widget(const std::string& text, toast_kind kind, float seconds); - // 0..1 opacity, from the fade-in/out. + // 0..1 opacity, from the fade-in/out. The whole toast (blur, + // strip and text) fades as one group; with group fades off the + // colours fade instead. void set_opacity(float a); + // Horizontal slide in UI px (positive = to the right of its + // resting place), from the slide-in/out. + void set_offset(float dx); + + private: + float offset = 0.0f; + + public: + virtual bool on_mouse_action(mouse_action action, mouse_button button, vec2 pos) override; }; diff --git a/sources/RenderSystem/GUI/Elements/Tree.cpp b/sources/RenderSystem/GUI/Elements/Tree.cpp index b6c2f251d..1df2a1d19 100644 --- a/sources/RenderSystem/GUI/Elements/Tree.cpp +++ b/sources/RenderSystem/GUI/Elements/Tree.cpp @@ -19,17 +19,64 @@ GUI::Elements::toogle_icon::toogle_icon() : vector_icon("chevron_right") height_size = size_type::FIXED; clickable = true; + thinkable = true; // turns the arrow } void GUI::Elements::toogle_icon::toogle(bool v) { - set_icon(v ? "chevron_right" : "chevron_down"); + set_expanded(!v); } -void GUI::Elements::line_base::draw(Context& c, bool selected) +void GUI::Elements::toogle_icon::show() +{ + const float eased = turn * turn * (3.0f - 2.0f * turn); + const int step = int(std::round(eased * 9.0f)); + + if (step == shown_step) + return; + + shown_step = step; + set_icon("chevron_" + std::to_string(step * 10)); +} + +void GUI::Elements::toogle_icon::set_expanded(bool expanded) +{ + turn_target = expanded ? 1.0f : 0.0f; + + if (!user_ui || !user_ui->window_animations) + { + turn = turn_target; + show(); + } +} + +void GUI::Elements::toogle_icon::think(float dt) +{ + if (turn == turn_target) + return; + + const float step = dt / 0.18f; // the same time as the branch it belongs to + turn = turn < turn_target ? std::min(turn + step, turn_target) : std::max(turn - step, turn_target); + show(); +} + +void GUI::Elements::draw_selection_bar(base::Context& c, const rect& box, float content_y, const SlidingHighlight& bar, float x, float w, float strength) +{ + if (bar.alpha <= 0.0f) + return; + + const float top = std::max(content_y + bar.y, box.y); + const float bottom = std::min(content_y + bar.y + bar.h, box.y + box.h); + if (bottom <= top) + return; + + float4 color = float4(53, 114, 202, 255) / 255.0f; + color.w *= bar.alpha * strength; + c.renderer->draw_color(c, color, rect(x, top, w, bottom - top)); +} + +void GUI::Elements::line_base::draw(Context& c) { if (dropping) c.renderer->draw_color(c, float4(153, 114, 202, 255) / 255.0f, get_render_bounds()); - else if (selected) - c.renderer->draw_color(c, float4(53, 114, 202, 255) / 255.0f, get_render_bounds()); } \ No newline at end of file diff --git a/sources/RenderSystem/GUI/Elements/Tree.ixx b/sources/RenderSystem/GUI/Elements/Tree.ixx index c13cfef5d..1f1a40369 100644 --- a/sources/RenderSystem/GUI/Elements/Tree.ixx +++ b/sources/RenderSystem/GUI/Elements/Tree.ixx @@ -22,7 +22,8 @@ export namespace GUI // Expand/collapse arrow: chevron_right when collapsed, chevron_down when - // expanded (toogle(true) = collapsed, matching the old skin textures). + // expanded, turning between the two (the chevron_0..chevron_90 icons) when + // it changes inside a UI. class toogle_icon : public vector_icon { public: @@ -30,21 +31,43 @@ export namespace GUI using ptr = s_ptr; std::function on_toogle; - + toogle_icon(); virtual bool on_mouse_action(mouse_action action, mouse_button button, vec2 pos) override; + // Inverted, as the callers that predate set_expanded use it: + // true = collapsed (right arrow), false = expanded (down arrow). void toogle(bool v); + + // Turns the arrow to the expanded (down) or collapsed (right) pose; + // without a UI, or with animations off, it jumps there. + void set_expanded(bool expanded); + + virtual void think(float dt) override; + + // 0 = pointing right (collapsed) ... 1 = pointing down (expanded). + float turn_amount() const { return turn; } + + private: + float turn = 0.0f; // 0 = right, 1 = down + float turn_target = 0.0f; + int shown_step = 0; // icon currently set, in 10-degree steps + void show(); }; + // Draws a SlidingHighlight bar (y and h relative to content_y) over x..x+w, + // clipped to box. Out of line: the Renderer is not visible from this file. + void draw_selection_bar(base::Context& c, const rect& box, float content_y, const SlidingHighlight& bar, float x, float w, float strength = 1.0f); + class line_base: public base { protected: bool dropping = false; - void draw(Context& c, bool selected); + // Only the drop target: the selection highlight is the tree's sliding bar. + void draw(Context& c); }; inline float tree_row_padding = 0; template @@ -58,7 +81,7 @@ export namespace GUI void draw(Context& c) { - line_base::draw(c, owner->selected); + line_base::draw(c); } void on_drop_leave(GUI::drag_n_drop_package::ptr) { @@ -107,6 +130,7 @@ export namespace GUI line(GUI::Elements::tree_element* owner) { this->owner = owner; + owner->row = this; docking = GUI::dock::TOP; height_size = GUI::size_type::MATCH_CHILDREN; width_size = GUI::size_type::MATCH_PARENT; @@ -159,10 +183,12 @@ export namespace GUI l->add_child(label_text); tree->add_child(l); - tree->on_toogle = [open_icon](bool v) - { + // A new element starts expanded. + open_icon->set_expanded(true); - open_icon->toogle(v); + tree->on_toogle = [open_icon](bool expanded) + { + open_icon->set_expanded(expanded); }; } @@ -201,9 +227,11 @@ export namespace GUI public: - + Events::Event on_toogle; bool selected = false; + // The element's own row (the line the creator built), for the tree's selection bar. + base* row = nullptr; TreeNode* node; using ptr = s_ptr; @@ -237,6 +265,7 @@ export namespace GUI tree_element(creator_type creator, TreeNode* elem) { this->creator = creator; + thinkable = true; // drives the expand / collapse animation height_size = size_type::MATCH_CHILDREN; width_size = size_type::MATCH_PARENT; size = { 100, 0 }; @@ -267,12 +296,107 @@ export namespace GUI } + // Expand / collapse. With animations on, the children slide open or + // shut: their height eases between 0 and the measured height while + // they fade as a group. A toggle mid-animation reverses it. void toogle() { - other->visible = !other->visible.get(); - on_toogle(other->visible); + expanded = !expanded; + on_toogle(expanded); + + if (!user_ui || !user_ui->window_animations) + { + other->visible = expanded; + return; + } + + anim_target = expanded ? 1.0f : 0.0f; + + if (animating) + return; // think() turns around + + if (expanded) + { + other->visible = true; + other->layer_opacity = 0.0f; // hidden until measured (a frame or two) + anim_s = 0.0f; + measure_frames = 2; + } + else + { + const float height = other->get_render_bounds().h / std::max(other->result_scale, 0.001f); + if (height <= 0.5f) + { + other->visible = false; + return; + } + anim_height = height; + other->height_size = size_type::FIXED; + other->size = vec2(other->size->x, height); + anim_s = 1.0f; + measure_frames = 0; + } + + animating = true; } - + + virtual void think(float dt) override + { + if (!animating) + return; + + if (measure_frames > 0) + { + if (--measure_frames > 0) + return; + + // The children are laid out at their natural height now. + if (anim_target <= 0.0f) // collapsed again while measuring + { + other->visible = false; + other->layer_opacity = 1.0f; + animating = false; + return; + } + + const float height = other->get_render_bounds().h / std::max(other->result_scale, 0.001f); + if (height <= 0.5f) + { + other->layer_opacity = 1.0f; + animating = false; + return; + } + anim_height = height; + other->height_size = size_type::FIXED; + other->size = vec2(other->size->x, 0.0f); + } + + constexpr float duration = 0.18f; + const float step = dt / duration; + anim_s = anim_s < anim_target ? std::min(anim_s + step, anim_target) : std::max(anim_s - step, anim_target); + + const float eased = anim_s * anim_s * (3.0f - 2.0f * anim_s); + other->size = vec2(other->size->x, anim_height * eased); + other->layer_opacity = eased; + + if (anim_s == anim_target) + { + other->height_size = size_type::MATCH_CHILDREN; + other->layer_opacity = 1.0f; + other->visible = anim_target > 0.0f; + animating = false; + } + } + + private: + bool expanded = true; + bool animating = false; + float anim_s = 1.0f; // 0 = collapsed, 1 = expanded + float anim_target = 1.0f; + float anim_height = 0.0f; // natural height of the children, UI px + int measure_frames = 0; + public: + }; @@ -304,10 +428,92 @@ export namespace GUI + // The selection highlight: one bar that slides to the selected row + // (x and width follow the row; y and height are eased, relative to + // the scrolled content). + SlidingHighlight bar; + float bar_x = 0.0f, bar_w = 0.0f; + base* bar_row = nullptr; // the row the bar is on, to tell a move from a new selection + // The row under the cursor: a fainter bar, same idea. + SlidingHighlight hover_bar; + float hover_x = 0.0f, hover_w = 0.0f; + base* hover_row = nullptr; + + virtual void on_pre_render(Context& c) override + { + scroll_container::on_pre_render(c); + + auto item = selected_item.lock(); + base* row = item ? item->row : nullptr; + + float y = 0.0f, h = 0.0f; + if (row) + { + const rect rb = row->get_render_bounds(); + y = rb.y - contents->get_render_bounds().y; + h = rb.h; + bar_x = rb.x; + bar_w = rb.w; + + // Same row, pushed by an expanding branch: stay on it. + if (row == bar_row) + bar.follow(y, h); + } + + bar_row = row; + const bool animate = user_ui && user_ui->window_animations; + bar.update(row != nullptr, y, h, c.delta_time, !animate); + + // The row under the cursor: the hit test finds a label or an + // icon, so go up to the nearest row of this tree. A nested + // tree's row is found first, as it should be. + base* hovered = nullptr; + if (user_ui) + if (auto hit = user_ui->get_hovered().lock()) + { + base* candidate = nullptr; + bool inside = false; + for (base* p = hit.get(); p; p = p->get_raw_parent()) + { + if (!candidate) + if (auto* l = dynamic_cast(p)) + candidate = l; + + if (p == this) + { + inside = true; + break; + } + } + + if (inside) + hovered = candidate; + } + + float hover_y = 0.0f, hover_h = 0.0f; + if (hovered) + { + const rect rb = hovered->get_render_bounds(); + hover_y = rb.y - contents->get_render_bounds().y; + hover_h = rb.h; + hover_x = rb.x; + hover_w = rb.w; + + if (hovered == hover_row) + hover_bar.follow(hover_y, hover_h); + } + + hover_row = hovered; + hover_bar.update(hovered != nullptr, hover_y, hover_h, c.delta_time, !animate); + } + void draw(Context& c) { /// context.renderer->draw_container(get_ptr(), c); - + // Under the rows, inside the tree. + const float content_y = contents->get_render_bounds().y; + draw_selection_bar(c, get_render_bounds(), content_y, hover_bar, hover_x, hover_w, 0.22f); + draw_selection_bar(c, get_render_bounds(), content_y, bar, bar_x, bar_w); } diff --git a/sources/RenderSystem/GUI/Elements/Window.cpp b/sources/RenderSystem/GUI/Elements/Window.cpp index ee920aa4e..e75b6bc0b 100644 --- a/sources/RenderSystem/GUI/Elements/Window.cpp +++ b/sources/RenderSystem/GUI/Elements/Window.cpp @@ -10,13 +10,22 @@ 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 }; + fade_on_add = true; + add_effect(); + add_effect(); + add_effect().inner = true; clickable = false; clip_child = true; label_text.reset(new label()); @@ -41,7 +50,7 @@ GUI::Elements::window::window() : GUI::Elements::resizable() close_button->docking = dock::RIGHT; close_button->on_click = [this](button::ptr) { - remove_from_parent(); + close(); }; title_bar->add_child(close_button); title_bar->add_child(label_text); @@ -77,6 +86,9 @@ void GUI::Elements::window::remove_child(base::ptr obj) void GUI::Elements::window::on_touch() { + if (is_fade_closing()) + return; + resizable::on_touch(); to_front(); } diff --git a/sources/RenderSystem/GUI/Elements/Window.ixx b/sources/RenderSystem/GUI/Elements/Window.ixx index b6af83dcb..f795a47ff 100644 --- a/sources/RenderSystem/GUI/Elements/Window.ixx +++ b/sources/RenderSystem/GUI/Elements/Window.ixx @@ -44,7 +44,10 @@ export namespace GUI virtual void on_touch() override; - + // Fades out and removes when the fade ends; see base::fade_close. + void close() { fade_close(); } + void cancel_close() { cancel_fade_close(); } + bool is_closing() const { return is_fade_closing(); } }; diff --git a/sources/RenderSystem/GUI/Renderer/Renderer.cpp b/sources/RenderSystem/GUI/Renderer/Renderer.cpp index 435383b55..f52a0f8a1 100644 --- a/sources/RenderSystem/GUI/Renderer/Renderer.cpp +++ b/sources/RenderSystem/GUI/Renderer/Renderer.cpp @@ -1,12 +1,543 @@ 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); + } + + // One snapshot texture per group-opacity layer slot, same lifetime + // handling as the blur textures. + struct LayerSlot + { + int width = 0; + int height = 0; + std::shared_ptr tex; + }; + + class LayerCache + { + std::mutex m; + std::vector slots; + std::vector> retired; + static constexpr size_t retire_depth = 24; + + public: + static LayerCache& get() + { + static LayerCache cache; + return cache; + } + + LayerSlot 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.tex || s.width != width || s.height != height) + { + if (s.tex) + retired.push_back(std::move(s.tex)); + + if (retired.size() > retire_depth) + retired.erase(retired.begin(), retired.begin() + (retired.size() - retire_depth)); + + s.width = width; + s.height = height; + s.tex = std::make_shared(device, + HAL::ResourceDesc::Tex2D(HAL::Format::R16G16B16A16_FLOAT, + { (UINT)width, (UINT)height }, 1, 1, HAL::ResFlags::ShaderResource), + HAL::TextureLayout::UNDEFINED); + s.tex->resource->set_name("ui_layer::snapshot"); + } + + return s; + } + + void reset() + { + std::lock_guard g(m); + slots.clear(); + retired.clear(); + } + }; + } + + void Renderer::reset_backdrop() + { + BackdropCache::get().reset(); + LayerCache::get().reset(); + } + + bool Renderer::begin_layer(base::Context& c, uint32_t slot_index, rect r, float pad_ui) + { + if (!c.target.resource || c.window_size.x <= 0 || c.window_size.y <= 0) + return false; + + PROFILE(L"ui_layer_begin"); + + const ivec2 target_size = c.target.get_size(); + const float k = float(target_size.x) / c.window_size.x; + const float pad = std::ceil(pad_ui * c.scale * k); + + const float2 lt = (float2(r.pos) + c.offset) * k; + const float2 rb = lt + float2(r.size) * k; + + int left = std::max(0, int(std::floor(lt.x - pad))); + int top = std::max(0, int(std::floor(lt.y - pad))); + const int right = std::min(target_size.x, int(std::ceil(rb.x + pad))); + const int bottom = std::min(target_size.y, int(std::ceil(rb.y + pad))); + int width = right - left; + int height = bottom - top; + + if (width < 2 || height < 2) + return false; + + // Same 32 px bucketing as the blur snapshots: elements of similar size + // share a texture size instead of reallocating. + constexpr int bucket = 32; + width = std::min(target_size.x, (width + bucket - 1) / bucket * bucket); + height = std::min(target_size.y, (height + bucket - 1) / bucket * bucket); + left = std::min(left, target_size.x - width); + top = std::min(top, target_size.y - height); + + // What the group sits on has to be in the target before it is copied. + nine_patch.flush(c); + + auto& device = RenderSystem::get().device(); + const LayerSlot slot = LayerCache::get().acquire(device, slot_index, width, height); + + c.command_list->get_copy().copy_texture(slot.tex->resource, ivec3(0, 0, 0), + c.target.resource, ivec3(left, top, 0), + ivec3(width, height, 1)); + + LayerRecord record; + record.left = left; + record.top = top; + record.width = width; + record.height = height; + record.view = slot.tex->texture_2d(); + record.owner = slot.tex; + layers.push_back(std::move(record)); + return true; + } + + void Renderer::end_layer(base::Context& c, float opacity) + { + if (layers.empty()) + return; + + const LayerRecord record = std::move(layers.back()); + layers.pop_back(); + + const float alpha = 1.0f - std::clamp(opacity, 0.0f, 1.0f); + if (alpha <= 0.0f) + return; + + PROFILE(L"ui_layer_end"); + + const ivec2 target_size = c.target.get_size(); + const float k = float(target_size.x) / c.window_size.x; + + // Window pixels, clipped like the group's own drawing was. + const float2 quad_lt = float2::max(float2(record.left / k, record.top / k), + float2(float(c.ui_clipping.left), float(c.ui_clipping.top))); + const float2 quad_rb = float2::min(float2((record.left + record.width) / k, (record.top + record.height) / k), + 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 group is batched; the copy goes over all of it. + nine_patch.flush(c); + + auto& graphics = c.command_list->get_graphics(); + + Slots::UI::UILayerRestore data; + data.GetSnapshot() = record.view.texture2D; + data.GetQuad() = float4(quad_lt.x, quad_lt.y, quad_rb.x - quad_lt.x, quad_rb.y - quad_lt.y); + data.GetWindow_size() = float2(c.window_size.x, c.window_size.y); + data.GetOrigin() = float2(float(record.left), float(record.top)); + data.GetAlpha() = alpha; + graphics.set(data); + + graphics.set_pipeline(); + graphics.set_topology(HAL::PrimitiveTopologyType::TRIANGLE, HAL::PrimitiveTopologyFeed::STRIP); + graphics.draw(4); + } + + 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; + + // Small radii skip the chain: the composite pass filters the snapshot + // itself (BackdropDraw::Direct), which saves two compute dispatches and + // the mip views and UAV transitions around them for every element. + constexpr float direct_radius_max = 8.0f; + const bool direct = radius_px >= 0.5f && radius_px <= direct_radius_max; + + if (!direct && 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; + data.GetLuminosity() = p.luminosity; + data.GetNoise() = p.noise; + // Binomial taps: sigma = one spacing, and the radius is about two sigma. + data.GetDirect() = float4(1.0f / region_w, 1.0f / region_h, std::max(radius_px * 0.5f, 0.25f), 0.0f); + graphics.set(data); + + graphics.set_pipeline(PSOS::UI::BackdropDraw::Direct.Use(direct)); + graphics.set_topology(HAL::PrimitiveTopologyType::TRIANGLE, HAL::PrimitiveTopologyFeed::STRIP); + graphics.draw(4); + } + } + + + void Renderer::draw_shadow(base::Context& c, rect r, const ShadowStyle& s) + { + if (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; + const bool ripple = p.ripple_phase >= 0.0f && p.ripple_strength > 0.0f; + if ((p.reveal_strength <= 0.0f && p.border_strength <= 0.0f && !shine && !ripple + && p.focus_strength <= 0.0f && p.inner_strength <= 0.0f) + || 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.GetRipple() = float4(p.ripple_origin.x, p.ripple_origin.y, ripple ? p.ripple_phase : -1.0f, p.ripple_strength); + data.GetFocus() = float4(p.focus_width * k, p.focus_strength, 0.0f, 0.0f); + data.GetFocus_color() = float4(p.focus_color.x, p.focus_color.y, p.focus_color.z, 1.0f); + data.GetInner() = float4(p.inner_size * k, p.inner_strength, p.inner_offset * k, 0.0f); + data.GetLight() = float4(p.light_color.x, p.light_color.y, p.light_color.z, 1.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) { diff --git a/sources/RenderSystem/GUI/Renderer/Renderer.ixx b/sources/RenderSystem/GUI/Renderer/Renderer.ixx index 1073ff92c..cbe017a75 100644 --- a/sources/RenderSystem/GUI/Renderer/Renderer.ixx +++ b/sources/RenderSystem/GUI/Renderer/Renderer.ixx @@ -29,10 +29,51 @@ 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; + 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 + float2 ripple_origin = { 0, 0 }; // window pixels + float ripple_phase = -1.0f; // 0..1 wave position, < 0 off + float ripple_strength = 0.0f; + float focus_strength = 0.0f; // accent ring just inside the edge + float focus_width = 2.0f; // UI px + float3 focus_color = { 0.21f, 0.45f, 0.79f }; // sRGB + float inner_strength = 0.0f; // darkening inside the edge + float inner_size = 10.0f; // falloff, UI px + float inner_offset = 2.0f; // pushes the shadow down from the top edge, UI px + float3 light_color = { 1.0f, 1.0f, 1.0f }; // sRGB; white suits dark surfaces, tint it on light ones + }; + class Renderer { NinePatch nine_patch; + struct LayerRecord + { + int left = 0, top = 0, width = 0, height = 0; // target pixels + HAL::Texture2DView view; // the snapshot + std::shared_ptr owner; // keeps its texture alive + }; + std::vector layers; + Texture area_tex; Texture virtual_tex; Texture container_tex; @@ -51,6 +92,38 @@ 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): + // 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); + + // 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); + + // Group opacity. begin_layer copies the target under `r` (grown by `pad` + // UI px, for shadows) and returns false when nothing can be snapshotted; + // everything drawn until the matching end_layer is the group. end_layer + // draws that copy back with alpha (1 - opacity), which leaves + // opacity * group + (1 - opacity) * what was below -- for any content, + // overlaps and blurs included. Begin and end must be recorded in the + // same command list (the draw list keeps a group in one chunk). + bool begin_layer(base::Context& c, uint32_t slot, rect r, float pad); + void end_layer(base::Context& c, float opacity); + + // 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/Platform/Window.cpp b/sources/Spectrum/Platform/Window.cpp index 394b0dc68..ee65d8e75 100644 --- a/sources/Spectrum/Platform/Window.cpp +++ b/sources/Spectrum/Platform/Window.cpp @@ -241,6 +241,13 @@ LRESULT Window::MsgProc(MSG msg) return 0; case WM_SETCURSOR: + // The default handler sets the class arrow on every mouse move, which then fights the + // cursor the UI picks (the edit box beam flickered to an arrow). Own the client area. + if (LOWORD(msg.lParam) == HTCLIENT && input_handler) + { + input_handler->apply_cursor(); + return TRUE; + } return DefWindowProcW(msg.hwnd, msg.message, msg.wParam, msg.lParam); case WM_GETMINMAXINFO: diff --git a/sources/Spectrum/main.cpp b/sources/Spectrum/main.cpp index 3e3f75686..91c97db3b 100644 --- a/sources/Spectrum/main.cpp +++ b/sources/Spectrum/main.cpp @@ -419,7 +419,7 @@ class material_settings_panel : public GUI::base materials::universal_material* m_material; ::FlowGraph::graph* m_graph; uint32_t m_shown_generation = ~0u; - GUI::Elements::MultiLineLabel::ptr m_text; + GUI::Elements::edit_text::ptr m_text; // Live 3D preview, docked at the very top of the panel -- same // SceneTextureRenderer/test-mesh mechanism asset_preview_content uses @@ -498,12 +498,14 @@ class material_settings_panel : public GUI::base add_child(row); } - // Own child, not inherited -- MultiLineLabel::on_text_changed() does - // contents->remove_all() on itself whenever .text is set, which - // would also wipe the checkboxes above if they were its children - // too. - m_text = std::make_shared(); - m_text->docking = GUI::dock::FILL; + // The generated pixel shader: HLSL-highlighted, selectable and copyable, + // but not editable -- it is rebuilt from the graph, so edits would be lost. + m_text = std::make_shared(Text::Style{ 13, Text::Weight::Normal, Text::Family::Mono }); + m_text->docking = GUI::dock::FILL; + m_text->multiline = true; + m_text->line_numbers = true; + m_text->read_only = true; + m_text->highlighter = GUI::Syntax::highlight_hlsl; add_child(m_text); } @@ -565,7 +567,7 @@ class material_settings_panel : public GUI::base m_shown_generation = m_material->preview_source_generation; auto src = m_material->get_pixel_shader_source(); - m_text->text = src.uniforms + src.text; + m_text->set_text(src.uniforms + src.text); } }; @@ -775,14 +777,18 @@ namespace GUI class debug_toolbar_panel : public GUI::base { public: - void draw(GUI::base::Context& c) override + debug_toolbar_panel() { - GUI::Texture shadow = Skin::get().Shadow; - shadow.margins = { 12, 12, 12, 12 }; - c.renderer->draw(c, shadow, get_render_bounds()); + // The combo boxes cover all but the 4 px padding, so they go glass too. + add_effect().glass_children = true; + add_effect(); + } + void draw(GUI::base::Context& c) override + { + // 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()); } }; @@ -2022,6 +2028,80 @@ class GraphRender : public Window, public GUI::user_interface } add_row("Toasts", toasts); + // Procedural shadow and glow (Properties > UI Effects). They are drawn + // outside the element, so the cards keep margins around them. + auto shadow_row = std::make_shared(); + auto add_panel = [&](GUI::base::ptr row, const std::string& caption, bool shadow, bool glow) + { + auto card = std::make_shared(); + card->color = float4(0.14f, 0.16f, 0.22f, 1.0f); + card->width_size = GUI::size_type::FIXED; + card->height_size = GUI::size_type::FIXED; + card->size = { 96, 44 }; + card->margin = { 8, 8, 22, 12 }; + if (shadow) + card->add_effect().style.radius = 4.0f; + if (glow) + { + auto& glow_effect = card->add_effect(); + glow_effect.style.color = float4(0.21f, 0.45f, 0.79f, 1.0f); + glow_effect.style.radius = 4.0f; + } + + auto text = std::make_shared