Skip to content

Commit 4dab9b5

Browse files
andreakarashoclaude
andcommitted
Add shadow support and fix rounded border rendering gap
Add ShadowConfig and shadow render commands for drop shadows on elements. Wire shadow support through all widget styles (Window, Panel, Layout, ScrollArea). Fix visible gap between rounded borders and content caused by Raylib's DrawRectangleRoundedLines shrinking the rect internally. Replace with manual border rendering using DrawRectangleRec for straight segments and DrawRing for corner arcs, which align precisely with the element bounds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6deae57 commit 4dab9b5

12 files changed

Lines changed: 451 additions & 76 deletions

File tree

src/Clay.Example/Program.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ void RenderHeader()
212212
Sizing = new Sizing(SizingAxis.Grow(), SizingAxis.Fixed(50)),
213213
Padding = Padding.Horizontal(16),
214214
BackgroundColor = ClayUI.Style.Window.TitleBarColor,
215-
CornerRadius = CornerRadius.All(8)
215+
CornerRadius = CornerRadius.All(8),
216+
Shadow = ShadowConfig.Drop(0, 1, 5, Color.Rgba(0, 0, 0, 50))
216217
});
217218

218219
ClayUI.Heading("Clay .NET", new HeadingStyle { TextColor = Color.Rgba(100, 180, 255) });
@@ -284,7 +285,8 @@ void RenderSidebar()
284285
SeparatorColor = ClayUI.Style.Panel.SeparatorColor,
285286
Border = ClayUI.Style.Panel.Border,
286287
Padding = Padding.All(12),
287-
ChildGap = 4
288+
ChildGap = 4,
289+
Shadow = ShadowConfig.Drop(1, 1, 6, Color.Rgba(0, 0, 0, 40))
288290
});
289291

290292
for (int i = 0; i < pages.Length; i++)
@@ -320,7 +322,8 @@ void RenderContent()
320322
{
321323
BackgroundColor = ClayUI.Style.Panel.BackgroundColor,
322324
Padding = Padding.All(20),
323-
CornerRadius = CornerRadius.All(8)
325+
CornerRadius = CornerRadius.All(8),
326+
Shadow = ShadowConfig.Drop(1, 1, 6, Color.Rgba(0, 0, 0, 40))
324327
});
325328

326329
ClayUI.Heading(pages[selectedPage]);

src/Clay.Example/RaylibRenderer.cs

Lines changed: 108 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ public void Render(ReadOnlySpan<RenderCommand> commands)
5959
case RenderCommandType.Custom:
6060
RenderCustom(box, cmd.Custom);
6161
break;
62+
63+
case RenderCommandType.Shadow:
64+
RenderShadow(box, cmd.Shadow);
65+
break;
6266
}
6367
}
6468
}
@@ -145,72 +149,58 @@ private void RenderBorder(BoundingBox box, BorderRenderData data)
145149
{
146150
var color = ToRayColor(data.Color);
147151

148-
// Use rounded border if corner radius is set
149152
if (data.CornerRadius.TopLeft > 0)
150153
{
151-
var rect = new Rectangle(box.X, box.Y, box.Width, box.Height);
152-
float minDimension = Math.Min(box.Width, box.Height);
153-
float roundness = (data.CornerRadius.TopLeft * 2) / minDimension;
154-
roundness = Math.Clamp(roundness, 0, 1);
155-
156-
// Use the maximum border width for the line thickness
157-
float lineThickness = Math.Max(
154+
float r = data.CornerRadius.TopLeft;
155+
float bw = Math.Max(
158156
Math.Max(data.Width.Top, data.Width.Bottom),
159-
Math.Max(data.Width.Left, data.Width.Right)
160-
);
161-
162-
Raylib.DrawRectangleRoundedLines(rect, roundness, 8, lineThickness, color);
157+
Math.Max(data.Width.Left, data.Width.Right));
158+
159+
// Clamp radius to half the smallest dimension
160+
r = Math.Min(r, Math.Min(box.Width, box.Height) / 2f);
161+
162+
// Draw four straight border segments (between the corner arcs)
163+
// Top
164+
Raylib.DrawRectangleRec(new Rectangle(box.X + r, box.Y, box.Width - 2 * r, bw), color);
165+
// Bottom
166+
Raylib.DrawRectangleRec(new Rectangle(box.X + r, box.Y + box.Height - bw, box.Width - 2 * r, bw), color);
167+
// Left
168+
Raylib.DrawRectangleRec(new Rectangle(box.X, box.Y + r, bw, box.Height - 2 * r), color);
169+
// Right
170+
Raylib.DrawRectangleRec(new Rectangle(box.X + box.Width - bw, box.Y + r, bw, box.Height - 2 * r), color);
171+
172+
// Draw four corner arcs using DrawRing (annulus sector)
173+
float outerR = r;
174+
float innerR = Math.Max(0, r - bw);
175+
int segments = 8;
176+
177+
// Top-left corner
178+
Raylib.DrawRing(
179+
new System.Numerics.Vector2(box.X + r, box.Y + r),
180+
innerR, outerR, 180, 270, segments, color);
181+
// Top-right corner
182+
Raylib.DrawRing(
183+
new System.Numerics.Vector2(box.X + box.Width - r, box.Y + r),
184+
innerR, outerR, 270, 360, segments, color);
185+
// Bottom-right corner
186+
Raylib.DrawRing(
187+
new System.Numerics.Vector2(box.X + box.Width - r, box.Y + box.Height - r),
188+
innerR, outerR, 0, 90, segments, color);
189+
// Bottom-left corner
190+
Raylib.DrawRing(
191+
new System.Numerics.Vector2(box.X + r, box.Y + box.Height - r),
192+
innerR, outerR, 90, 180, segments, color);
163193
}
164194
else
165195
{
166-
// Fall back to straight borders
167-
// Top border
168196
if (data.Width.Top > 0)
169-
{
170-
Raylib.DrawRectangle(
171-
(int)box.X,
172-
(int)box.Y,
173-
(int)box.Width,
174-
(int)data.Width.Top,
175-
color
176-
);
177-
}
178-
179-
// Bottom border
197+
Raylib.DrawRectangle((int)box.X, (int)box.Y, (int)box.Width, (int)data.Width.Top, color);
180198
if (data.Width.Bottom > 0)
181-
{
182-
Raylib.DrawRectangle(
183-
(int)box.X,
184-
(int)(box.Y + box.Height - data.Width.Bottom),
185-
(int)box.Width,
186-
(int)data.Width.Bottom,
187-
color
188-
);
189-
}
190-
191-
// Left border
199+
Raylib.DrawRectangle((int)box.X, (int)(box.Y + box.Height - data.Width.Bottom), (int)box.Width, (int)data.Width.Bottom, color);
192200
if (data.Width.Left > 0)
193-
{
194-
Raylib.DrawRectangle(
195-
(int)box.X,
196-
(int)box.Y,
197-
(int)data.Width.Left,
198-
(int)box.Height,
199-
color
200-
);
201-
}
202-
203-
// Right border
201+
Raylib.DrawRectangle((int)box.X, (int)box.Y, (int)data.Width.Left, (int)box.Height, color);
204202
if (data.Width.Right > 0)
205-
{
206-
Raylib.DrawRectangle(
207-
(int)(box.X + box.Width - data.Width.Right),
208-
(int)box.Y,
209-
(int)data.Width.Right,
210-
(int)box.Height,
211-
color
212-
);
213-
}
203+
Raylib.DrawRectangle((int)(box.X + box.Width - data.Width.Right), (int)box.Y, (int)data.Width.Right, (int)box.Height, color);
214204
}
215205
}
216206

@@ -525,6 +515,68 @@ private void RenderTextInput(BoundingBox box, TextInputWidget widget)
525515
PopScissor();
526516
}
527517

518+
private void RenderShadow(BoundingBox box, ShadowRenderData data)
519+
{
520+
// The bounding box arrives pre-expanded (offset + blur + spread already applied).
521+
// We draw concentric rounded rectangles from outermost (full box) to innermost,
522+
// each layer covering a ring of the blur. Alpha per layer = base_alpha / layers,
523+
// so they accumulate to full opacity at the center where all layers overlap.
524+
float blur = data.BlurRadius;
525+
526+
if (blur <= 0)
527+
{
528+
// Sharp shadow — single rectangle
529+
var rect = new Rectangle(box.X, box.Y, box.Width, box.Height);
530+
var color = ToRayColor(data.Color);
531+
532+
if (data.CornerRadius.TopLeft > 0)
533+
{
534+
float minDim = Math.Min(box.Width, box.Height);
535+
float roundness = Math.Clamp((data.CornerRadius.TopLeft * 2) / minDim, 0, 1);
536+
Raylib.DrawRectangleRounded(rect, roundness, 8, color);
537+
}
538+
else
539+
{
540+
Raylib.DrawRectangleRec(rect, color);
541+
}
542+
return;
543+
}
544+
545+
// +1 so the last layer sits exactly at inset=blur (the original element edge)
546+
int layers = Math.Clamp((int)blur, 4, 24) + 1;
547+
float perLayerAlpha = data.Color.A / layers;
548+
byte r = (byte)Math.Clamp(data.Color.R, 0, 255);
549+
byte g = (byte)Math.Clamp(data.Color.G, 0, 255);
550+
byte b = (byte)Math.Clamp(data.Color.B, 0, 255);
551+
byte alpha = (byte)Math.Clamp(perLayerAlpha, 1, 255);
552+
553+
for (int i = 0; i < layers; i++)
554+
{
555+
float inset = blur * ((float)i / (layers - 1));
556+
var rect = new Rectangle(
557+
box.X + inset,
558+
box.Y + inset,
559+
box.Width - inset * 2,
560+
box.Height - inset * 2
561+
);
562+
563+
if (rect.width <= 0 || rect.height <= 0) continue;
564+
565+
var layerColor = new RayColor { r = r, g = g, b = b, a = alpha };
566+
567+
if (data.CornerRadius.TopLeft > 0)
568+
{
569+
float minDim = Math.Min(rect.width, rect.height);
570+
float roundness = Math.Clamp((data.CornerRadius.TopLeft * 2) / minDim, 0, 1);
571+
Raylib.DrawRectangleRounded(rect, roundness, 8, layerColor);
572+
}
573+
else
574+
{
575+
Raylib.DrawRectangleRec(rect, layerColor);
576+
}
577+
}
578+
}
579+
528580
private static RayColor ToRayColor(ClayColor color)
529581
{
530582
return new RayColor

src/Clay.GameEditor/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
// Apply dark theme by default (game editors are always dark)
4545
ClayUI.Style = ClayUIStyle.Dark;
4646

47+
// Add shadows to windows for depth
48+
ClayUI.Style.Window = new WindowStyle
49+
{
50+
Shadow = ShadowConfig.Drop(0, 3, 10, Color.Rgba(0, 0, 0, 80))
51+
}.MergeOver(ClayUI.Style.Window);
52+
4753
// ============ Editor Color Palette ============
4854

4955
var colBg = Color.Rgba(30, 30, 30);
@@ -459,7 +465,8 @@ void RenderToolbar()
459465
Sizing = new Sizing(SizingAxis.Grow(), SizingAxis.Fixed(36)),
460466
Padding = Padding.Symmetric(8, 4),
461467
BackgroundColor = colToolbar,
462-
Border = new BorderConfig { Width = new BorderWidth(0, 0, 0, 1), Color = colBorder }
468+
Border = new BorderConfig { Width = new BorderWidth(0, 0, 0, 1), Color = colBorder },
469+
Shadow = ShadowConfig.Drop(0, 1, 4, Color.Rgba(0, 0, 0, 35))
463470
});
464471

465472
var toolBtnStyle = new ButtonStyle

src/Clay.GameEditor/RaylibRenderer.cs

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ public void Render(ReadOnlySpan<RenderCommand> commands)
5959
case RenderCommandType.Custom:
6060
RenderCustom(box, cmd.Custom);
6161
break;
62+
63+
case RenderCommandType.Shadow:
64+
RenderShadow(box, cmd.Shadow);
65+
break;
6266
}
6367
}
6468
}
@@ -129,13 +133,45 @@ private void RenderBorder(BoundingBox box, BorderRenderData data)
129133

130134
if (data.CornerRadius.TopLeft > 0)
131135
{
132-
var rect = new Rectangle(box.X, box.Y, box.Width, box.Height);
133-
float minDimension = Math.Min(box.Width, box.Height);
134-
float roundness = Math.Clamp((data.CornerRadius.TopLeft * 2) / minDimension, 0, 1);
135-
float lineThickness = Math.Max(
136+
float r = data.CornerRadius.TopLeft;
137+
float bw = Math.Max(
136138
Math.Max(data.Width.Top, data.Width.Bottom),
137139
Math.Max(data.Width.Left, data.Width.Right));
138-
Raylib.DrawRectangleRoundedLines(rect, roundness, 8, lineThickness, color);
140+
141+
// Clamp radius to half the smallest dimension
142+
r = Math.Min(r, Math.Min(box.Width, box.Height) / 2f);
143+
144+
// Draw four straight border segments (between the corner arcs)
145+
// Top
146+
Raylib.DrawRectangleRec(new Rectangle(box.X + r, box.Y, box.Width - 2 * r, bw), color);
147+
// Bottom
148+
Raylib.DrawRectangleRec(new Rectangle(box.X + r, box.Y + box.Height - bw, box.Width - 2 * r, bw), color);
149+
// Left
150+
Raylib.DrawRectangleRec(new Rectangle(box.X, box.Y + r, bw, box.Height - 2 * r), color);
151+
// Right
152+
Raylib.DrawRectangleRec(new Rectangle(box.X + box.Width - bw, box.Y + r, bw, box.Height - 2 * r), color);
153+
154+
// Draw four corner arcs using DrawRing (annulus sector)
155+
float outerR = r;
156+
float innerR = Math.Max(0, r - bw);
157+
int segments = 8;
158+
159+
// Top-left corner
160+
Raylib.DrawRing(
161+
new System.Numerics.Vector2(box.X + r, box.Y + r),
162+
innerR, outerR, 180, 270, segments, color);
163+
// Top-right corner
164+
Raylib.DrawRing(
165+
new System.Numerics.Vector2(box.X + box.Width - r, box.Y + r),
166+
innerR, outerR, 270, 360, segments, color);
167+
// Bottom-right corner
168+
Raylib.DrawRing(
169+
new System.Numerics.Vector2(box.X + box.Width - r, box.Y + box.Height - r),
170+
innerR, outerR, 0, 90, segments, color);
171+
// Bottom-left corner
172+
Raylib.DrawRing(
173+
new System.Numerics.Vector2(box.X + r, box.Y + box.Height - r),
174+
innerR, outerR, 90, 180, segments, color);
139175
}
140176
else
141177
{
@@ -376,6 +412,62 @@ private void RenderTextInput(BoundingBox box, TextInputWidget widget)
376412
PopScissor();
377413
}
378414

415+
private void RenderShadow(BoundingBox box, ShadowRenderData data)
416+
{
417+
float blur = data.BlurRadius;
418+
419+
if (blur <= 0)
420+
{
421+
var rect = new Rectangle(box.X, box.Y, box.Width, box.Height);
422+
var color = ToRayColor(data.Color);
423+
424+
if (data.CornerRadius.TopLeft > 0)
425+
{
426+
float minDim = Math.Min(box.Width, box.Height);
427+
float roundness = Math.Clamp((data.CornerRadius.TopLeft * 2) / minDim, 0, 1);
428+
Raylib.DrawRectangleRounded(rect, roundness, 8, color);
429+
}
430+
else
431+
{
432+
Raylib.DrawRectangleRec(rect, color);
433+
}
434+
return;
435+
}
436+
437+
int layers = Math.Clamp((int)blur, 4, 24) + 1;
438+
float perLayerAlpha = data.Color.A / layers;
439+
byte r = (byte)Math.Clamp(data.Color.R, 0, 255);
440+
byte g = (byte)Math.Clamp(data.Color.G, 0, 255);
441+
byte b = (byte)Math.Clamp(data.Color.B, 0, 255);
442+
byte alpha = (byte)Math.Clamp(perLayerAlpha, 1, 255);
443+
444+
for (int i = 0; i < layers; i++)
445+
{
446+
float inset = blur * ((float)i / (layers - 1));
447+
var rect = new Rectangle(
448+
box.X + inset,
449+
box.Y + inset,
450+
box.Width - inset * 2,
451+
box.Height - inset * 2
452+
);
453+
454+
if (rect.width <= 0 || rect.height <= 0) continue;
455+
456+
var layerColor = new RayColor { r = r, g = g, b = b, a = alpha };
457+
458+
if (data.CornerRadius.TopLeft > 0)
459+
{
460+
float minDim = Math.Min(rect.width, rect.height);
461+
float roundness = Math.Clamp((data.CornerRadius.TopLeft * 2) / minDim, 0, 1);
462+
Raylib.DrawRectangleRounded(rect, roundness, 8, layerColor);
463+
}
464+
else
465+
{
466+
Raylib.DrawRectangleRec(rect, layerColor);
467+
}
468+
}
469+
}
470+
379471
private static RayColor ToRayColor(ClayColor color)
380472
{
381473
return new RayColor

0 commit comments

Comments
 (0)