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.

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 {