Added full clay border support to draw (#28)
Co-authored-by: Zachary Levy <zachary@sunforge.is> Reviewed-on: #28
This commit was merged in pull request #28.
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
package examples
|
||||
|
||||
import "core:os"
|
||||
import sdl "vendor:sdl3"
|
||||
|
||||
import "../../draw"
|
||||
import "../../vendor/clay"
|
||||
import cyber "../cybersteel"
|
||||
|
||||
// Clay border debug example.
|
||||
//
|
||||
// Lays out a grid of bordered Clay elements that exercise every code path in
|
||||
// `clay_emit_partial_border` and `try_dispatch_clay_rect_border_pair`:
|
||||
//
|
||||
// 1. Uniform borders (fast path) — sharp, rounded, and the border-thicker-than-radius
|
||||
// edge case (inner corner clamps to 0).
|
||||
// 2. Background + border combinations — opaque bg + opaque uniform border MERGES into one
|
||||
// SDF primitive; translucent border DECLINES the merge to preserve blend fidelity;
|
||||
// non-uniform border declines and falls through to the slow path; translucent bg with
|
||||
// opaque border still merges (bg alpha doesn't affect merge correctness).
|
||||
// 3. Single-side borders — top / right / bottom / left individually.
|
||||
// 4. Two-side borders — parallel pairs (no corners drawn) and adjacent pairs (one corner
|
||||
// rounds, others stay square).
|
||||
// 5. Three-side borders + asymmetric widths.
|
||||
// 6. Layout correctness — a vertical list with bottom-border separators (each border
|
||||
// lives inside its own item, no bleed between siblings) and a row of adjacent fully
|
||||
// bordered siblings (no border overlap, each in its own bounds).
|
||||
clay_borders :: proc() {
|
||||
if !sdl.Init({.VIDEO}) do os.exit(1)
|
||||
window := sdl.CreateWindow("Clay Borders Debug", 1200, 900, {.HIGH_PIXEL_DENSITY})
|
||||
gpu := sdl.CreateGPUDevice(draw.PLATFORM_SHADER_FORMAT, true, nil)
|
||||
if !sdl.ClaimWindowForGPUDevice(gpu, window) do os.exit(1)
|
||||
if !draw.init(gpu, window) do os.exit(1)
|
||||
PLEX_SANS_REGULAR = draw.register_font(cyber.SANS_REGULAR_RAW)
|
||||
|
||||
// Distinct colors so the fill, border, and translucent variants are visually unambiguous.
|
||||
BG_PAGE :: draw.Color{25, 25, 30, 255}
|
||||
FILL_OPAQUE :: draw.Color{80, 120, 200, 255}
|
||||
FILL_TRANSLUCENT :: draw.Color{80, 120, 200, 128}
|
||||
BORDER_OPAQUE :: draw.Color{255, 200, 100, 255}
|
||||
BORDER_TRANSLUCENT :: draw.Color{255, 200, 100, 128}
|
||||
|
||||
label_config := clay.TextElementConfig {
|
||||
fontId = PLEX_SANS_REGULAR,
|
||||
fontSize = 12,
|
||||
textColor = {220, 220, 220, 255},
|
||||
}
|
||||
header_config := clay.TextElementConfig {
|
||||
fontId = PLEX_SANS_REGULAR,
|
||||
fontSize = 16,
|
||||
textColor = {255, 255, 255, 255},
|
||||
}
|
||||
title_config := clay.TextElementConfig {
|
||||
fontId = PLEX_SANS_REGULAR,
|
||||
fontSize = 22,
|
||||
textColor = {255, 255, 255, 255},
|
||||
}
|
||||
|
||||
for {
|
||||
defer free_all(context.temp_allocator)
|
||||
ev: sdl.Event
|
||||
for sdl.PollEvent(&ev) {
|
||||
if ev.type == .QUIT do return
|
||||
}
|
||||
|
||||
base_layer := draw.begin({width = 1200, height = 900})
|
||||
clay.SetLayoutDimensions({width = base_layer.bounds.width, height = base_layer.bounds.height})
|
||||
clay.BeginLayout()
|
||||
|
||||
if clay.UI(clay.ID("borders_page"))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingGrow({}), clay.SizingGrow({})},
|
||||
padding = clay.PaddingAll(20),
|
||||
childGap = 14,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
backgroundColor = clay_color(BG_PAGE),
|
||||
},
|
||||
) {
|
||||
clay.Text("Clay Borders Debug", title_config)
|
||||
|
||||
//----- Section 1: Uniform borders (fast path) -----------------------------------
|
||||
clay.Text("Uniform borders (fast path)", header_config)
|
||||
if clay.UI(clay.ID("row_uniform"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"1px sharp",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 1, right = 1, top = 1, bottom = 1},
|
||||
{},
|
||||
)
|
||||
border_test_card(
|
||||
"2px, radius 8",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 2, right = 2, top = 2, bottom = 2},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"8px, radius 20",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 8, right = 8, top = 8, bottom = 8},
|
||||
{topLeft = 20, topRight = 20, bottomRight = 20, bottomLeft = 20},
|
||||
)
|
||||
border_test_card(
|
||||
"10px > radius 5 (inner clamps)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 10, right = 10, top = 10, bottom = 10},
|
||||
{topLeft = 5, topRight = 5, bottomRight = 5, bottomLeft = 5},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 2: Background + border (merge optimization) ----------------------
|
||||
clay.Text("Background + border (merge optimization)", header_config)
|
||||
if clay.UI(clay.ID("row_bg_border"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"opaque bg + opaque (MERGES: 1 prim)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 2, right = 2, top = 2, bottom = 2},
|
||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
||||
)
|
||||
border_test_card(
|
||||
"translucent bg + opaque (MERGES)",
|
||||
label_config,
|
||||
FILL_TRANSLUCENT,
|
||||
BORDER_OPAQUE,
|
||||
{left = 3, right = 3, top = 3, bottom = 3},
|
||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
||||
)
|
||||
border_test_card(
|
||||
"opaque bg + translucent (NO merge)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_TRANSLUCENT,
|
||||
{left = 4, right = 4, top = 4, bottom = 4},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"opaque bg + non-uniform (NO merge)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 1, right = 4, top = 2, bottom = 3},
|
||||
{topLeft = 6, topRight = 6, bottomRight = 6, bottomLeft = 6},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 3: Single side borders -------------------------------------------
|
||||
clay.Text("Single side", header_config)
|
||||
if clay.UI(clay.ID("row_single_side"))(border_row_layout()) {
|
||||
border_test_card("top only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {top = 4}, {})
|
||||
border_test_card("right only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {right = 4}, {})
|
||||
border_test_card(
|
||||
"bottom only (4px, divider)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{bottom = 4},
|
||||
{},
|
||||
)
|
||||
border_test_card("left only (4px)", label_config, FILL_OPAQUE, BORDER_OPAQUE, {left = 4}, {})
|
||||
}
|
||||
|
||||
//----- Section 4: Two side borders ----------------------------------------------
|
||||
clay.Text("Two sides", header_config)
|
||||
if clay.UI(clay.ID("row_two_sides"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"T+B parallel (no corners)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, bottom = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"L+R parallel (no corners)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{left = 3, right = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"T+L adjacent (TL rounds)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, left = 3},
|
||||
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
|
||||
)
|
||||
border_test_card(
|
||||
"B+R adjacent (BR rounds)",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{bottom = 3, right = 3},
|
||||
{topLeft = 12, topRight = 12, bottomRight = 12, bottomLeft = 12},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 5: Three sides + asymmetric widths -------------------------------
|
||||
clay.Text("Three sides + asymmetric widths", header_config)
|
||||
if clay.UI(clay.ID("row_advanced"))(border_row_layout()) {
|
||||
border_test_card(
|
||||
"T+R+B (no L), rounded",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, right = 3, bottom = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"T+L+R (no B), rounded",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 3, left = 3, right = 3},
|
||||
{topLeft = 8, topRight = 8, bottomRight = 8, bottomLeft = 8},
|
||||
)
|
||||
border_test_card(
|
||||
"asym 1/2/3/4 T/R/B/L",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 1, right = 2, bottom = 3, left = 4},
|
||||
{},
|
||||
)
|
||||
border_test_card(
|
||||
"asym + rounded",
|
||||
label_config,
|
||||
FILL_OPAQUE,
|
||||
BORDER_OPAQUE,
|
||||
{top = 2, right = 4, bottom = 2, left = 4},
|
||||
{topLeft = 10, topRight = 10, bottomRight = 10, bottomLeft = 10},
|
||||
)
|
||||
}
|
||||
|
||||
//----- Section 6: Layout correctness --------------------------------------------
|
||||
clay.Text("Layout correctness", header_config)
|
||||
if clay.UI(clay.ID("row_correctness"))(
|
||||
{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 14}},
|
||||
) {
|
||||
// 6a: vertical list with per-item bottom-border separator. Each item's
|
||||
// border draws INSIDE its own bounds, so adjacent items don't bleed.
|
||||
if clay.UI(clay.ID("list_demo"))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingFixed(300), clay.SizingFit({})},
|
||||
padding = clay.PaddingAll(6),
|
||||
childGap = 6,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
},
|
||||
) {
|
||||
clay.Text("List with bottom-border separators", label_config)
|
||||
if clay.UI(clay.ID("list_outer"))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, layoutDirection = .TopToBottom},
|
||||
backgroundColor = clay_color(FILL_OPAQUE),
|
||||
},
|
||||
) {
|
||||
for index in 0 ..< 5 {
|
||||
if clay.UI(clay.ID("list_item", u32(index)))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(28)}, padding = clay.PaddingAll(6)},
|
||||
border = {color = clay_color(BORDER_OPAQUE), width = {bottom = 1}},
|
||||
},
|
||||
) {
|
||||
clay.Text("Item", label_config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6b: row of adjacent fully bordered siblings. With borders rendered
|
||||
// INSIDE each element's bounds, the boundary between two siblings shows
|
||||
// the natural 2*width sum (no overlap, no bleed).
|
||||
if clay.UI(clay.ID("adj_demo"))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingFixed(380), clay.SizingFit({})},
|
||||
padding = clay.PaddingAll(6),
|
||||
childGap = 6,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
},
|
||||
) {
|
||||
clay.Text("Adjacent bordered siblings (no gap)", label_config)
|
||||
if clay.UI(clay.ID("adj_row"))({layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}}}) {
|
||||
for index in 0 ..< 4 {
|
||||
if clay.UI(clay.ID("adj_item", u32(index)))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingFixed(80), clay.SizingFixed(60)}},
|
||||
backgroundColor = clay_color(FILL_OPAQUE),
|
||||
border = {color = clay_color(BORDER_OPAQUE), width = {left = 2, right = 2, top = 2, bottom = 2}},
|
||||
},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clay_batch := draw.ClayBatch {
|
||||
bounds = base_layer.bounds,
|
||||
cmds = clay.EndLayout(0),
|
||||
}
|
||||
draw.prepare_clay_batch(base_layer, &clay_batch, {0, 0})
|
||||
draw.end(gpu, window)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: convert a draw.Color (RGBA u8) to clay.Color (RGBA float in 0-255 range).
|
||||
clay_color :: proc(c: draw.Color) -> clay.Color {
|
||||
return clay.Color{f32(c[0]), f32(c[1]), f32(c[2]), f32(c[3])}
|
||||
}
|
||||
|
||||
// Helper: shared row container declaration for the test sections.
|
||||
border_row_layout :: proc() -> clay.ElementDeclaration {
|
||||
return clay.ElementDeclaration{layout = {sizing = {clay.SizingGrow({}), clay.SizingFit({})}, childGap = 12}}
|
||||
}
|
||||
|
||||
// One labeled test card: a fixed-width column with a caption above and a sample bordered
|
||||
// rectangle below. Uses `clay.ID_LOCAL` for the inner element so each card gets a unique
|
||||
// child ID without the caller passing one explicitly.
|
||||
border_test_card :: proc(
|
||||
label: string,
|
||||
label_config: clay.TextElementConfig,
|
||||
fill_color: draw.Color,
|
||||
border_color: draw.Color,
|
||||
border_width: clay.BorderWidth,
|
||||
corner_radii: clay.CornerRadius,
|
||||
) {
|
||||
if clay.UI(clay.ID(label))(
|
||||
{
|
||||
layout = {
|
||||
sizing = {clay.SizingFixed(275), clay.SizingFit({})},
|
||||
padding = clay.PaddingAll(4),
|
||||
childGap = 6,
|
||||
layoutDirection = .TopToBottom,
|
||||
},
|
||||
},
|
||||
) {
|
||||
clay.Text(label, label_config)
|
||||
if clay.UI(clay.ID_LOCAL("test_inner"))(
|
||||
{
|
||||
layout = {sizing = {clay.SizingGrow({}), clay.SizingFixed(64)}},
|
||||
backgroundColor = clay_color(fill_color),
|
||||
border = clay.BorderElementConfig{color = clay_color(border_color), width = border_width},
|
||||
cornerRadius = corner_radii,
|
||||
},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ EX_HELLOPE_SHAPES :: "hellope-shapes"
|
||||
EX_HELLOPE_TEXT :: "hellope-text"
|
||||
EX_HELLOPE_CLAY :: "hellope-clay"
|
||||
EX_HELLOPE_CUSTOM :: "hellope-custom"
|
||||
EX_CLAY_BORDERS :: "clay-borders"
|
||||
EX_TEXTURES :: "textures"
|
||||
EX_GAUSSIAN_BLUR :: "gaussian-blur"
|
||||
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
|
||||
@@ -23,6 +24,8 @@ AVAILABLE_EXAMPLES_MSG ::
|
||||
", " +
|
||||
EX_HELLOPE_CUSTOM +
|
||||
", " +
|
||||
EX_CLAY_BORDERS +
|
||||
", " +
|
||||
EX_TEXTURES +
|
||||
", " +
|
||||
EX_GAUSSIAN_BLUR +
|
||||
@@ -81,6 +84,7 @@ main :: proc() {
|
||||
case EX_HELLOPE_CUSTOM: hellope_custom()
|
||||
case EX_HELLOPE_SHAPES: hellope_shapes()
|
||||
case EX_HELLOPE_TEXT: hellope_text()
|
||||
case EX_CLAY_BORDERS: clay_borders()
|
||||
case EX_TEXTURES: textures()
|
||||
case EX_GAUSSIAN_BLUR: gaussian_blur()
|
||||
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
|
||||
|
||||
@@ -166,12 +166,14 @@ textures :: proc() {
|
||||
|
||||
ROW2_Y :: f32(190)
|
||||
|
||||
// QR code (RGBA texture with baked colors, nearest sampling)
|
||||
// QR code (RGBA texture with baked colors, nearest sampling) + thin framing border.
|
||||
draw.rectangle(base_layer, {COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, draw.Color{255, 255, 255, 255}) // white bg
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
draw.Texture_Fill{id = qr_texture, tint = draw.WHITE, uv_rect = {0, 0, 1, 1}, sampler = .Nearest_Clamp},
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = 2,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
@@ -182,7 +184,7 @@ textures :: proc() {
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Rounded corners
|
||||
// Rounded corners + outline traces the rounded shape.
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
||||
@@ -192,6 +194,8 @@ textures :: proc() {
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
outline_color = draw.Color{255, 200, 100, 255},
|
||||
outline_width = 3,
|
||||
radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3),
|
||||
)
|
||||
draw.text(
|
||||
@@ -203,7 +207,7 @@ textures :: proc() {
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Rotating
|
||||
// Rotating + outline rotates with the texture.
|
||||
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
@@ -214,6 +218,8 @@ textures :: proc() {
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = 2,
|
||||
origin = draw.center_of(rot_rect),
|
||||
rotation = spin_angle,
|
||||
)
|
||||
@@ -282,7 +288,7 @@ textures :: proc() {
|
||||
color = draw.WHITE,
|
||||
)
|
||||
|
||||
// Per-corner radii
|
||||
// Per-corner radii + outline traces the asymmetric corner shape.
|
||||
draw.rectangle(
|
||||
base_layer,
|
||||
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
|
||||
@@ -292,6 +298,8 @@ textures :: proc() {
|
||||
uv_rect = {0, 0, 1, 1},
|
||||
sampler = .Nearest_Clamp,
|
||||
},
|
||||
outline_color = draw.Color{255, 100, 100, 255},
|
||||
outline_width = 3,
|
||||
radii = {20, 0, 20, 0},
|
||||
)
|
||||
draw.text(
|
||||
@@ -321,12 +329,14 @@ textures :: proc() {
|
||||
sampler = .Nearest_Clamp,
|
||||
}
|
||||
|
||||
// Textured circle
|
||||
// Textured circle + outline (textured shape with built-in border).
|
||||
draw.circle(
|
||||
base_layer,
|
||||
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||
SHAPE_SIZE / 2,
|
||||
checker_fill,
|
||||
outline_color = draw.WHITE,
|
||||
outline_width = 2,
|
||||
)
|
||||
draw.text(
|
||||
base_layer,
|
||||
|
||||
Reference in New Issue
Block a user