From 414ef624793de072d3312dea9b1588c4ca83c67a Mon Sep 17 00:00:00 2001 From: Aiden Date: Wed, 6 May 2026 12:38:23 +1000 Subject: [PATCH] Added clock time --- .gitea/workflows/ci.yml | 20 +++++ CMakeLists.txt | 18 +++++ README.md | 5 +- SHADER_CONTRACT.md | 4 + .../LoopThroughWithOpenGLCompositing.vcxproj | 2 + ...roughWithOpenGLCompositing.vcxproj.filters | 6 ++ .../gl/GlobalParamsBuffer.cpp | 2 + .../runtime/RuntimeClock.cpp | 45 +++++++++++ .../runtime/RuntimeClock.h | 12 +++ .../runtime/RuntimeHost.cpp | 5 ++ .../shader/ShaderTypes.h | 2 + runtime/templates/shader_wrapper.slang.in | 6 ++ shaders/utc-clock/shader.json | 36 +++++++++ shaders/utc-clock/shader.slang | 75 +++++++++++++++++++ tests/RuntimeClockTests.cpp | 50 +++++++++++++ tests/Std140BufferTests.cpp | 8 +- ui/src/styles.css | 57 ++++++++++---- 17 files changed, 335 insertions(+), 18 deletions(-) create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h create mode 100644 shaders/utc-clock/shader.json create mode 100644 shaders/utc-clock/shader.slang create mode 100644 tests/RuntimeClockTests.cpp diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ee07ba3..22711d7 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -18,6 +18,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Verify Visual Studio ATL + shell: powershell + run: | + $atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue) + if ($atlHeaders.Count -eq 0) { + Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service." + exit 1 + } + Write-Host "Found ATL header: $($atlHeaders[0].FullName)" + - name: Configure Debug shell: powershell run: | @@ -92,6 +102,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Verify Visual Studio ATL + shell: powershell + run: | + $atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue) + if ($atlHeaders.Count -eq 0) { + Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service." + exit 1 + } + Write-Host "Found ATL header: $($atlHeaders[0].FullName)" + - name: Build UI shell: powershell working-directory: ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 75118ba..e99203d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,8 @@ set(APP_SOURCES "${APP_DIR}/resource.h" "${APP_DIR}/runtime/RuntimeHost.cpp" "${APP_DIR}/runtime/RuntimeHost.h" + "${APP_DIR}/runtime/RuntimeClock.cpp" + "${APP_DIR}/runtime/RuntimeClock.h" "${APP_DIR}/runtime/RuntimeJson.cpp" "${APP_DIR}/runtime/RuntimeJson.h" "${APP_DIR}/runtime/RuntimeParameterUtils.cpp" @@ -158,6 +160,22 @@ endif() enable_testing() add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests) +add_executable(RuntimeClockTests + "${APP_DIR}/runtime/RuntimeClock.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp" +) + +target_include_directories(RuntimeClockTests PRIVATE + "${APP_DIR}" + "${APP_DIR}/runtime" +) + +if(MSVC) + target_compile_options(RuntimeClockTests PRIVATE /W3) +endif() + +add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests) + add_executable(RuntimeParameterUtilsTests "${APP_DIR}/runtime/RuntimeJson.cpp" "${APP_DIR}/runtime/RuntimeParameterUtils.cpp" diff --git a/README.md b/README.md index de87641..33d5ae5 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,8 @@ The Windows jobs validate native third-party dependencies before configuring CMa - `GPUDIRECT_DIR`: path to `Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect`. - `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`. +The Windows runner also needs the Visual Studio ATL component installed. In Visual Studio Build Tools 2022, add `C++ ATL for latest v143 build tools (x86 & x64)`, component ID `Microsoft.VisualStudio.Component.VC.ATL`. + Example runner paths: ```text @@ -249,8 +251,7 @@ If neither variable is set, the workflow falls back to the repo-local defaults u - Genlock. - Find a better UI library. - Logs. -- Continue source cleanup/refactoring. +- Continue source cleanup/refactoring. Pass 1 done - Display the control URL in the Windows app, ideally clickable, without rendering it on the video output. - Support a separate sound shader `.slang` file in shader packages. -- Add runtime date/time uniforms using UTC and the PC's local offset. ![alt text](image.png) diff --git a/SHADER_CONTRACT.md b/SHADER_CONTRACT.md index 58b2305..25ff680 100644 --- a/SHADER_CONTRACT.md +++ b/SHADER_CONTRACT.md @@ -122,6 +122,8 @@ struct ShaderContext float2 inputResolution; float2 outputResolution; float time; + float utcTimeSeconds; + float utcOffsetSeconds; float frameCount; float mixAmount; float bypass; @@ -137,6 +139,8 @@ Fields: - `inputResolution`: decoded input video resolution in pixels. - `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode. - `time`: elapsed runtime time in seconds. +- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight. +- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day. - `frameCount`: incrementing frame counter. - `mixAmount`: runtime mix amount. - `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`. diff --git a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj index ab541a0..bead286 100644 --- a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj +++ b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj @@ -209,6 +209,7 @@ + @@ -224,6 +225,7 @@ + diff --git a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters index 7d368eb..35ec3e1 100644 --- a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters +++ b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters @@ -54,6 +54,9 @@ Source Files + + Source Files + @@ -95,6 +98,9 @@ Header Files + + Header Files + diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/GlobalParamsBuffer.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/GlobalParamsBuffer.cpp index 1f611a6..776c95a 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/GlobalParamsBuffer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/GlobalParamsBuffer.cpp @@ -18,6 +18,8 @@ bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availa AppendStd140Float(buffer, static_cast(state.timeSeconds)); AppendStd140Vec2(buffer, static_cast(state.inputWidth), static_cast(state.inputHeight)); AppendStd140Vec2(buffer, static_cast(state.outputWidth), static_cast(state.outputHeight)); + AppendStd140Float(buffer, static_cast(state.utcTimeSeconds)); + AppendStd140Float(buffer, static_cast(state.utcOffsetSeconds)); AppendStd140Float(buffer, static_cast(state.frameCount)); AppendStd140Float(buffer, static_cast(state.mixAmount)); AppendStd140Float(buffer, static_cast(state.bypass)); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.cpp new file mode 100644 index 0000000..46678b3 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.cpp @@ -0,0 +1,45 @@ +#include "RuntimeClock.h" + +#include + +namespace +{ +bool ToUtcTime(std::time_t time, std::tm& utcTime) +{ + return gmtime_s(&utcTime, &time) == 0; +} + +bool ToLocalTime(std::time_t time, std::tm& localTime) +{ + return localtime_s(&localTime, &time) == 0; +} +} + +RuntimeClockSnapshot GetRuntimeClockSnapshot() +{ + return MakeRuntimeClockSnapshot(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); +} + +RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now) +{ + RuntimeClockSnapshot snapshot; + + std::tm utcTime = {}; + if (!ToUtcTime(now, utcTime)) + return snapshot; + + snapshot.utcTimeSeconds = + static_cast(utcTime.tm_hour * 3600 + utcTime.tm_min * 60 + utcTime.tm_sec); + + std::tm localTime = {}; + if (!ToLocalTime(now, localTime)) + return snapshot; + + utcTime.tm_isdst = localTime.tm_isdst; + const std::time_t localAsTime = std::mktime(&localTime); + const std::time_t utcAsLocalTime = std::mktime(&utcTime); + if (localAsTime != static_cast(-1) && utcAsLocalTime != static_cast(-1)) + snapshot.utcOffsetSeconds = std::difftime(localAsTime, utcAsLocalTime); + + return snapshot; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h new file mode 100644 index 0000000..809fd44 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeClock.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +struct RuntimeClockSnapshot +{ + double utcTimeSeconds = 0.0; + double utcOffsetSeconds = 0.0; +}; + +RuntimeClockSnapshot GetRuntimeClockSnapshot(); +RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp index d30c562..1ac9538 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp @@ -1,5 +1,7 @@ #include "stdafx.h" #include "RuntimeHost.h" + +#include "RuntimeClock.h" #include "RuntimeParameterUtils.h" #include "ShaderCompiler.h" #include "ShaderPackageRegistry.h" @@ -1321,6 +1323,7 @@ bool RuntimeHost::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputH void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector& states) const { + const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot(); for (const LayerPersistentState& layer : mPersistentState.layers) { auto shaderIt = mPackagesById.find(layer.shaderId); @@ -1331,6 +1334,8 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou state.layerId = layer.id; state.shaderId = layer.shaderId; state.timeSeconds = std::chrono::duration_cast>(std::chrono::steady_clock::now() - mStartTime).count(); + state.utcTimeSeconds = clock.utcTimeSeconds; + state.utcOffsetSeconds = clock.utcOffsetSeconds; state.frameCount = static_cast(mFrameCounter); state.mixAmount = 1.0; state.bypass = layer.bypass ? 1.0 : 0.0; diff --git a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h index 58b2c13..a8118c6 100644 --- a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h +++ b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h @@ -102,6 +102,8 @@ struct RuntimeRenderState std::vector textureAssets; std::vector fontAssets; double timeSeconds = 0.0; + double utcTimeSeconds = 0.0; + double utcOffsetSeconds = 0.0; double frameCount = 0.0; double mixAmount = 1.0; double bypass = 0.0; diff --git a/runtime/templates/shader_wrapper.slang.in b/runtime/templates/shader_wrapper.slang.in index 1b7836a..60f9f45 100644 --- a/runtime/templates/shader_wrapper.slang.in +++ b/runtime/templates/shader_wrapper.slang.in @@ -11,6 +11,8 @@ struct ShaderContext float2 inputResolution; float2 outputResolution; float time; + float utcTimeSeconds; + float utcOffsetSeconds; float frameCount; float mixAmount; float bypass; @@ -23,6 +25,8 @@ cbuffer GlobalParams float gTime; float2 gInputResolution; float2 gOutputResolution; + float gUtcTimeSeconds; + float gUtcOffsetSeconds; float gFrameCount; float gMixAmount; float gBypass; @@ -80,6 +84,8 @@ float4 fragmentMain(FragmentInput input) : SV_Target context.inputResolution = gInputResolution; context.outputResolution = gOutputResolution; context.time = gTime; + context.utcTimeSeconds = gUtcTimeSeconds; + context.utcOffsetSeconds = gUtcOffsetSeconds; context.frameCount = gFrameCount; context.mixAmount = gMixAmount; context.bypass = gBypass; diff --git a/shaders/utc-clock/shader.json b/shaders/utc-clock/shader.json new file mode 100644 index 0000000..da756da --- /dev/null +++ b/shaders/utc-clock/shader.json @@ -0,0 +1,36 @@ +{ + "id": "utc-clock", + "name": "UTC Clock", + "description": "Shows an analog clock driven by the host PC clock. Uses UTC seconds plus the PC UTC offset exposed to shaders.", + "category": "Utility", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "showLocalTime", + "label": "Show Local Time", + "type": "bool", + "default": true + }, + { + "id": "clockScale", + "label": "Clock Scale", + "type": "float", + "default": 0.7, + "min": 0.25, + "max": 0.95, + "step": 0.01 + }, + { + "id": "faceColor", + "label": "Face Color", + "type": "color", + "default": [0.03, 0.04, 0.05, 0.82] + }, + { + "id": "accentColor", + "label": "Accent Color", + "type": "color", + "default": [0.1, 0.62, 0.86, 1.0] + } + ] +} diff --git a/shaders/utc-clock/shader.slang b/shaders/utc-clock/shader.slang new file mode 100644 index 0000000..6797e64 --- /dev/null +++ b/shaders/utc-clock/shader.slang @@ -0,0 +1,75 @@ +float secondsOfDay(float seconds) +{ + return seconds - floor(seconds / 86400.0) * 86400.0; +} + +float lineMask(float2 p, float angle, float innerRadius, float outerRadius, float width) +{ + float2 direction = float2(sin(angle), cos(angle)); + float along = dot(p, direction); + float across = length(p - direction * along); + float body = smoothstep(width, width * 0.35, across); + float start = smoothstep(innerRadius - width, innerRadius + width, along); + float end = 1.0 - smoothstep(outerRadius - width, outerRadius + width, along); + return body * start * end; +} + +float ringMask(float radius, float target, float width) +{ + return smoothstep(width, 0.0, abs(radius - target)); +} + +float tickMask(float2 p, int tick) +{ + float angle = (float(tick) / 60.0) * 6.28318530718; + float2 direction = float2(sin(angle), cos(angle)); + float along = dot(p, direction); + float across = length(p - direction * along); + float major = (tick % 5) == 0 ? 1.0 : 0.0; + float inner = lerp(0.82, 0.76, major); + float outer = 0.9; + float width = lerp(0.006, 0.011, major); + float body = smoothstep(width, width * 0.35, across); + float start = smoothstep(inner, inner + 0.025, along); + float end = 1.0 - smoothstep(outer - 0.025, outer, along); + return body * start * end; +} + +float4 shadeVideo(ShaderContext context) +{ + float2 aspect = float2(context.outputResolution.x / max(context.outputResolution.y, 1.0), 1.0); + float2 p = (context.uv * 2.0 - 1.0) * aspect / max(clockScale, 0.01); + float radius = length(p); + + float seconds = showLocalTime + ? secondsOfDay(context.utcTimeSeconds + context.utcOffsetSeconds) + : secondsOfDay(context.utcTimeSeconds); + float hour = floor(seconds / 3600.0); + float minute = floor((seconds - hour * 3600.0) / 60.0); + float second = seconds - hour * 3600.0 - minute * 60.0; + + float secondAngle = second / 60.0 * 6.28318530718; + float minuteAngle = (minute + second / 60.0) / 60.0 * 6.28318530718; + float hour12 = hour - floor(hour / 12.0) * 12.0; + float hourAngle = (hour12 + minute / 60.0) / 12.0 * 6.28318530718; + + float4 color = context.sourceColor; + float face = smoothstep(1.0, 0.97, radius); + color = lerp(color, faceColor, face * faceColor.a); + + float ring = ringMask(radius, 0.92, 0.01); + float ticks = 0.0; + for (int i = 0; i < 60; ++i) + ticks = max(ticks, tickMask(p, i)); + + float hourHand = lineMask(p, hourAngle, -0.05, 0.48, 0.028); + float minuteHand = lineMask(p, minuteAngle, -0.07, 0.72, 0.018); + float secondHand = lineMask(p, secondAngle, -0.12, 0.8, 0.009); + float hub = smoothstep(0.07, 0.0, radius); + + float whiteDetail = max(max(ring, ticks), max(hourHand, minuteHand)); + color.rgb = lerp(color.rgb, float3(0.92, 0.96, 1.0), whiteDetail); + color.rgb = lerp(color.rgb, accentColor.rgb, max(secondHand, hub) * accentColor.a); + + return color; +} diff --git a/tests/RuntimeClockTests.cpp b/tests/RuntimeClockTests.cpp new file mode 100644 index 0000000..6b274a0 --- /dev/null +++ b/tests/RuntimeClockTests.cpp @@ -0,0 +1,50 @@ +#include "RuntimeClock.h" + +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const char* message) +{ + if (condition) + return; + + std::cerr << "FAIL: " << message << "\n"; + ++gFailures; +} + +void TestUtcSecondsOfDay() +{ + const RuntimeClockSnapshot midnight = MakeRuntimeClockSnapshot(0); + Expect(midnight.utcTimeSeconds == 0.0, "Unix epoch starts at UTC midnight"); + + const RuntimeClockSnapshot midday = MakeRuntimeClockSnapshot(12 * 3600 + 34 * 60 + 56); + Expect(midday.utcTimeSeconds == 45296.0, "UTC time of day is seconds since midnight"); +} + +void TestOffsetLooksLikeTimezoneOffset() +{ + const RuntimeClockSnapshot snapshot = MakeRuntimeClockSnapshot(12 * 3600); + Expect(std::fmod(snapshot.utcOffsetSeconds, 60.0) == 0.0, "UTC offset is minute-aligned"); + Expect(snapshot.utcOffsetSeconds >= -14.0 * 3600.0 && snapshot.utcOffsetSeconds <= 14.0 * 3600.0, + "UTC offset is in the normal timezone range"); +} +} + +int main() +{ + TestUtcSecondsOfDay(); + TestOffsetLooksLikeTimezoneOffset(); + + if (gFailures != 0) + { + std::cerr << gFailures << " RuntimeClock test failure(s).\n"; + return 1; + } + + std::cout << "RuntimeClock tests passed.\n"; + return 0; +} diff --git a/tests/Std140BufferTests.cpp b/tests/Std140BufferTests.cpp index 323bf27..a7db2cf 100644 --- a/tests/Std140BufferTests.cpp +++ b/tests/Std140BufferTests.cpp @@ -62,6 +62,8 @@ void TestGlobalParamStylePacking() AppendStd140Float(buffer, 10.0f); // time AppendStd140Vec2(buffer, 1920.0f, 1080.0f); // input resolution AppendStd140Vec2(buffer, 1280.0f, 720.0f); // output resolution + AppendStd140Float(buffer, 45296.0f); // UTC time of day + AppendStd140Float(buffer, 36000.0f); // UTC offset AppendStd140Float(buffer, 42.0f); // frame count AppendStd140Float(buffer, 0.5f); // mix AppendStd140Float(buffer, 1.0f); // bypass @@ -78,8 +80,10 @@ void TestGlobalParamStylePacking() Expect(ReadFloat(buffer, 0) == 10.0f, "time is at the start of the block"); Expect(ReadFloat(buffer, 8) == 1920.0f, "first vec2 aligns after scalar padding"); Expect(ReadFloat(buffer, 16) == 1280.0f, "second vec2 follows first vec2"); - Expect(ReadInt(buffer, 36) == 3, "history length scalar remains tightly packed"); - Expect(ReadFloat(buffer, 48) == 4.0f, "vec2 shader parameter aligns to 8 bytes"); + Expect(ReadFloat(buffer, 24) == 45296.0f, "UTC time follows output resolution"); + Expect(ReadFloat(buffer, 28) == 36000.0f, "UTC offset follows UTC time"); + Expect(ReadInt(buffer, 44) == 3, "history length scalar remains tightly packed"); + Expect(ReadFloat(buffer, 56) == 4.0f, "vec2 shader parameter aligns to 8 bytes"); Expect(ReadFloat(buffer, 64) == 0.1f, "color parameter aligns to 16 bytes"); Expect(ReadInt(buffer, 80) == 1, "boolean parameter follows vec4"); Expect(ReadInt(buffer, 84) == 2, "enum parameter follows boolean"); diff --git a/ui/src/styles.css b/ui/src/styles.css index a7fba78..46a3aa0 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -17,6 +17,8 @@ --app-radius-sm: 4px; --app-space: 1rem; --app-container: 980px; + --control-height: 42px; + --button-min-width: 7.25rem; --app-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --app-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; background: var(--app-bg); @@ -96,12 +98,11 @@ h4 { text-transform: uppercase; } -button, input[type="number"], input[type="text"], select { width: 100%; - min-height: 38px; + min-height: var(--control-height); border: 1px solid var(--app-border); background: var(--app-surface-2); color: inherit; @@ -135,6 +136,11 @@ button:focus-visible { } button { + width: auto; + min-width: var(--button-min-width); + min-height: var(--control-height); + padding: 0.65rem 1rem; + border: 1px solid transparent; border-color: transparent; background: var(--app-primary); color: #fff; @@ -201,7 +207,7 @@ pre { .panel__header button { width: auto; - min-width: 9rem; + min-width: var(--button-min-width); } .dashboard-grid, @@ -459,7 +465,7 @@ pre { } .stack-panel__reload { - min-width: 8.25rem; + min-width: 8.75rem; } .toolbar__group { @@ -469,10 +475,16 @@ pre { } .toolbar__inline { - grid-template-columns: minmax(0, 1fr) minmax(7.5rem, 0.28fr); + grid-template-columns: minmax(0, 1fr) auto; + align-items: stretch; gap: 0.5rem; } +.toolbar__inline button { + width: auto; + min-width: var(--button-min-width); +} + .layer-stack { display: grid; gap: 0.75rem; @@ -532,17 +544,21 @@ pre { .layer-card__actions { justify-content: flex-end; + align-self: flex-start; } .layer-card__actions button, .layer-card__subheader button { width: auto; - min-width: 5.25rem; + min-width: var(--button-min-width); + height: var(--control-height); } .icon-button { - width: 38px; - min-width: 38px; + width: var(--control-height); + min-width: var(--control-height); + height: var(--control-height); + min-height: var(--control-height); padding: 0; display: inline-flex; align-items: center; @@ -585,6 +601,7 @@ pre { width: 34px; height: 34px; min-width: 34px; + min-height: 34px; padding: 0; border-color: transparent; background: transparent; @@ -654,6 +671,7 @@ pre { } .shader-picker__trigger { + width: 100%; display: flex; align-items: center; justify-content: space-between; @@ -713,6 +731,7 @@ pre { } .shader-picker__option { + width: 100%; display: grid; gap: 0.25rem; min-height: 4.25rem; @@ -1015,8 +1034,7 @@ pre { } .dashboard-grid, - .stack-panel__grid, - .toolbar__inline { + .stack-panel__grid { grid-template-columns: 1fr; } @@ -1025,10 +1043,17 @@ pre { } .panel__header button, - .toolbar__inline button, .stack-panel__reload, - .layer-card__actions, - .layer-card__actions button, + .layer-card__actions button { + width: auto; + } + + .layer-card__actions { + width: auto; + align-self: flex-end; + justify-content: flex-end; + } + .layer-card__field select { width: 100%; } @@ -1049,8 +1074,12 @@ pre { grid-template-columns: 1fr; } + .toolbar__inline { + grid-template-columns: minmax(0, 1fr) auto; + } + .parameter__reset { - width: 100%; + width: 24px; } .parameter__swatch {