Added full clay border support to draw
This commit is contained in:
@@ -75,6 +75,11 @@
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Run draw clay-borders example",
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- clay-borders",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
},
|
||||
{
|
||||
"label": "Run draw gaussian-blur example",
|
||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
|
||||
|
||||
+369
-35
@@ -171,6 +171,7 @@ Global :: struct {
|
||||
|
||||
// -- Clay (once per frame in prepare_clay_batch) --
|
||||
clay_memory: [^]u8, // Raw memory block backing Clay's internal arena.
|
||||
clay_merge_open_stack: [dynamic]Clay_Merge_Candidate, // Pending Rectangle/Image primitives waiting for a matching Border to merge with.
|
||||
|
||||
// -- Text (occasional — font registration and text cache lookups) --
|
||||
text_cache: Text_Cache, // Font registry, SDL_ttf engine, and cached TTF_Text objects.
|
||||
@@ -381,6 +382,7 @@ init :: proc(
|
||||
),
|
||||
tmp_sub_batches = make([dynamic]Sub_Batch, 0, BUFFER_INIT_SIZE, allocator = allocator),
|
||||
tmp_uncached_text = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||||
clay_merge_open_stack = make([dynamic]Clay_Merge_Candidate, 0, 16, allocator = allocator),
|
||||
tmp_gaussian_blur_primitives = make(
|
||||
[dynamic]Gaussian_Blur_Primitive,
|
||||
0,
|
||||
@@ -482,6 +484,7 @@ clear_global :: proc() {
|
||||
clear(&GLOB.tmp_primitives)
|
||||
clear(&GLOB.tmp_sub_batches)
|
||||
clear(&GLOB.tmp_gaussian_blur_primitives)
|
||||
clear(&GLOB.clay_merge_open_stack)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
@@ -812,6 +815,29 @@ Backdrop_Marker :: struct {
|
||||
feather_ppx: f32,
|
||||
}
|
||||
|
||||
// One entry on the Clay merge stack. Pushed by `dispatch_clay_command` when emitting a
|
||||
// Rectangle or an Image primitive, then popped by a matching Border to retroactively add
|
||||
// the outline. See `try_dispatch_clay_border_merge` for the matching semantics.
|
||||
//INTERNAL
|
||||
Clay_Merge_Candidate :: struct {
|
||||
primitive_index: u32, // Index into `GLOB.tmp_primitives` of the candidate primitive.
|
||||
outer_bounds: Rectangle, // Clay's bounding box — keyed on for the bounds match check.
|
||||
corner_radii: clay.CornerRadius, // Clay's corner radii — also keyed on for the match check.
|
||||
image_data: Clay_Image_Data, // Only read when kind == .Fill_Texture (needed to refit UVs to inner_bounds).
|
||||
kind: Clay_Merge_Candidate_Kind,
|
||||
}
|
||||
|
||||
//INTERNAL
|
||||
Clay_Merge_Candidate_Kind :: enum u8 {
|
||||
// Solid Color brush. Used for Rectangle commands and for the bg primitive of an Image
|
||||
// command that has `backgroundColor.a > 0`. Merge mutation: shrink shape + add outline.
|
||||
Fill_Color,
|
||||
// Texture_Fill brush. Used for the image primitive of an Image command with no bg, where
|
||||
// `fit_params` returned `fit_rect == outer_bounds` (the image fully covers Clay's bounds).
|
||||
// Merge mutation: shrink shape + add outline + refit UV against inner_bounds.
|
||||
Fill_Texture,
|
||||
}
|
||||
|
||||
// Returns true if this Clay render command represents a backdrop primitive — i.e. its
|
||||
// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`.
|
||||
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
||||
@@ -822,6 +848,263 @@ is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// Emit a Clay border drawn INSIDE `bounds` — the outer edge of each side aligns with
|
||||
// `bounds`, the inner edge is `border_width.*` pixels inset. Matches Clay's layout model
|
||||
// (CSS border-box) so the visible element occupies exactly Clay's allocated space.
|
||||
//
|
||||
// The fast path (uniform widths) uses `rectangle()` with the built-in SDF outline, which
|
||||
// always extends outward from the shape it's given — we pre-shrink the shape by
|
||||
// `border_width` so the outline lands precisely at Clay's bounds. The slow path (non-uniform
|
||||
// widths) emits per-side rectangles and per-corner arcs directly, all positioned inside
|
||||
// `bounds`. All-zero widths is a no-op.
|
||||
//
|
||||
// A corner is rounded iff its radius is positive AND both adjacent sides have positive
|
||||
// width. Top corners take their thickness from `border_width.top`, bottom corners from
|
||||
// `border_width.bottom`. When the two widths meeting at a corner differ there is a step at
|
||||
// the side/corner junction (acceptable for the rare mixed-width case).
|
||||
//
|
||||
// When `border_width > corner_radius`, the inner corner clamps to zero (sharp inside, still
|
||||
// rounded outside) — matches CSS-standard behavior.
|
||||
//INTERNAL
|
||||
clay_emit_partial_border :: proc(
|
||||
layer: ^Layer,
|
||||
bounds: Rectangle,
|
||||
border_color: Color,
|
||||
border_width: clay.BorderWidth,
|
||||
corner_radii: clay.CornerRadius,
|
||||
) {
|
||||
// All-zero: nothing to draw.
|
||||
if border_width.top == 0 && border_width.right == 0 && border_width.bottom == 0 && border_width.left == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert side widths once (u16 -> f32) and cache for reuse.
|
||||
width_top := f32(border_width.top)
|
||||
width_right := f32(border_width.right)
|
||||
width_bottom := f32(border_width.bottom)
|
||||
width_left := f32(border_width.left)
|
||||
|
||||
// Fast path: all four sides have the same nonzero width. Pre-shrink the shape by the
|
||||
// uniform width so the SDF outline (which always extends outward from the shape) lands
|
||||
// exactly at Clay's `bounds` — the visible border ends up INSIDE Clay's allocation while
|
||||
// the SDF mechanism keeps doing outward outlining. Single SDF primitive, exact curves,
|
||||
// analytical AA.
|
||||
if border_width.left == border_width.top &&
|
||||
border_width.top == border_width.right &&
|
||||
border_width.right == border_width.bottom {
|
||||
uniform_width := width_top
|
||||
inner_bounds := Rectangle {
|
||||
x = bounds.x + uniform_width,
|
||||
y = bounds.y + uniform_width,
|
||||
width = bounds.width - 2 * uniform_width,
|
||||
height = bounds.height - 2 * uniform_width,
|
||||
}
|
||||
inner_radii := Rectangle_Radii {
|
||||
top_left = max(0, corner_radii.topLeft - uniform_width),
|
||||
top_right = max(0, corner_radii.topRight - uniform_width),
|
||||
bottom_right = max(0, corner_radii.bottomRight - uniform_width),
|
||||
bottom_left = max(0, corner_radii.bottomLeft - uniform_width),
|
||||
}
|
||||
rectangle(
|
||||
layer,
|
||||
inner_bounds,
|
||||
BLANK,
|
||||
outline_color = border_color,
|
||||
outline_width = uniform_width,
|
||||
radii = inner_radii,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// A corner is drawn rounded only if its radius is positive AND both adjacent sides are present.
|
||||
top_left_rounded := corner_radii.topLeft > 0 && border_width.top > 0 && border_width.left > 0
|
||||
top_right_rounded := corner_radii.topRight > 0 && border_width.top > 0 && border_width.right > 0
|
||||
bottom_left_rounded := corner_radii.bottomLeft > 0 && border_width.bottom > 0 && border_width.left > 0
|
||||
bottom_right_rounded := corner_radii.bottomRight > 0 && border_width.bottom > 0 && border_width.right > 0
|
||||
|
||||
// Horizontal x-coordinates where the top/bottom side rectangles start/end. When the
|
||||
// adjacent corner is rounded, the side stops at `bounds.x + radius` (where the corner
|
||||
// arc takes over). When not rounded, the side runs to the bounds edge; the perpendicular
|
||||
// side handles the inset to avoid overlap.
|
||||
top_left_x: f32 = top_left_rounded ? bounds.x + corner_radii.topLeft : bounds.x
|
||||
top_right_x: f32 =
|
||||
top_right_rounded ? bounds.x + bounds.width - corner_radii.topRight : bounds.x + bounds.width
|
||||
bottom_left_x: f32 = bottom_left_rounded ? bounds.x + corner_radii.bottomLeft : bounds.x
|
||||
bottom_right_x: f32 =
|
||||
bottom_right_rounded ? bounds.x + bounds.width - corner_radii.bottomRight : bounds.x + bounds.width
|
||||
|
||||
// Vertical y-coordinates where the left/right side rectangles start/end. When the
|
||||
// adjacent corner is rounded, inset by the corner radius. When not rounded, inset by the
|
||||
// adjacent horizontal width — the horizontal side owns the corner area (extending through
|
||||
// it to the bounds edge), so the vertical side starts below it to avoid overdraw of
|
||||
// translucent colors.
|
||||
top_left_y: f32 = top_left_rounded ? bounds.y + corner_radii.topLeft : bounds.y + width_top
|
||||
top_right_y: f32 = top_right_rounded ? bounds.y + corner_radii.topRight : bounds.y + width_top
|
||||
bottom_left_y: f32 =
|
||||
bottom_left_rounded ? bounds.y + bounds.height - corner_radii.bottomLeft : bounds.y + bounds.height - width_bottom
|
||||
bottom_right_y: f32 =
|
||||
bottom_right_rounded ? bounds.y + bounds.height - corner_radii.bottomRight : bounds.y + bounds.height - width_bottom
|
||||
|
||||
// Side rectangles drawn INSIDE `bounds`. Sharp corners, solid fill, no outline. Each
|
||||
// gated on its own width — skipping zero-width sides saves the primitive upload.
|
||||
if border_width.top > 0 {
|
||||
top_side := Rectangle {
|
||||
x = top_left_x,
|
||||
y = bounds.y,
|
||||
width = top_right_x - top_left_x,
|
||||
height = width_top,
|
||||
}
|
||||
rectangle(layer, top_side, border_color)
|
||||
}
|
||||
if border_width.bottom > 0 {
|
||||
bottom_side := Rectangle {
|
||||
x = bottom_left_x,
|
||||
y = bounds.y + bounds.height - width_bottom,
|
||||
width = bottom_right_x - bottom_left_x,
|
||||
height = width_bottom,
|
||||
}
|
||||
rectangle(layer, bottom_side, border_color)
|
||||
}
|
||||
if border_width.left > 0 {
|
||||
left_side := Rectangle {
|
||||
x = bounds.x,
|
||||
y = top_left_y,
|
||||
width = width_left,
|
||||
height = bottom_left_y - top_left_y,
|
||||
}
|
||||
rectangle(layer, left_side, border_color)
|
||||
}
|
||||
if border_width.right > 0 {
|
||||
right_side := Rectangle {
|
||||
x = bounds.x + bounds.width - width_right,
|
||||
y = top_right_y,
|
||||
width = width_right,
|
||||
height = bottom_right_y - top_right_y,
|
||||
}
|
||||
rectangle(layer, right_side, border_color)
|
||||
}
|
||||
|
||||
// Corner arcs (90° quadrants) drawn INSIDE bounds: outer radius matches Clay's
|
||||
// `corner_radii`, inner radius is the outer radius minus the relevant border thickness
|
||||
// (clamped to 0 for thick borders — produces a filled pie slice when border > radius,
|
||||
// matching CSS). Angle convention matches ring(): 0° = +x (right), 90° = +y (down),
|
||||
// 180° = -x (left), 270° = -y (up).
|
||||
if top_left_rounded {
|
||||
radius := corner_radii.topLeft
|
||||
inner_radius := max(0, radius - width_top)
|
||||
center := Vec2{bounds.x + radius, bounds.y + radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 180, end_angle = 270)
|
||||
}
|
||||
if top_right_rounded {
|
||||
radius := corner_radii.topRight
|
||||
inner_radius := max(0, radius - width_top)
|
||||
center := Vec2{bounds.x + bounds.width - radius, bounds.y + radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 270, end_angle = 360)
|
||||
}
|
||||
if bottom_right_rounded {
|
||||
radius := corner_radii.bottomRight
|
||||
inner_radius := max(0, radius - width_bottom)
|
||||
center := Vec2{bounds.x + bounds.width - radius, bounds.y + bounds.height - radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 0, end_angle = 90)
|
||||
}
|
||||
if bottom_left_rounded {
|
||||
radius := corner_radii.bottomLeft
|
||||
inner_radius := max(0, radius - width_bottom)
|
||||
center := Vec2{bounds.x + radius, bounds.y + bounds.height - radius}
|
||||
ring(layer, center, inner_radius, radius, border_color, start_angle = 90, end_angle = 180)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to retroactively merge this Border into a pending Rectangle/Image candidate on the
|
||||
// merge stack. Returns true on success so the caller can skip the standalone Border emission.
|
||||
//
|
||||
// Clay emits a parent element's bg and border bracketing all the children's commands, so a
|
||||
// simple "is the next command a Border?" check (the previous approach) only catches leaf
|
||||
// elements. The stack approach lets us pair them across arbitrary nesting: every Rectangle/
|
||||
// Image push registers itself; every Border pops down until it finds a geometric match.
|
||||
//
|
||||
// Pop semantics: non-matching candidates above the match are discarded — their elements had
|
||||
// no border anyway, so their primitives stay in `tmp_primitives` as plain Rectangles. A
|
||||
// Border that finds no match at all falls back to standalone `clay_emit_partial_border`.
|
||||
//
|
||||
// Predicates that decline a candidate:
|
||||
// - non-uniform or zero border widths (can't be a single uniform outline)
|
||||
// - translucent border (the unmerged path's bg-under-border blending differs)
|
||||
// - mismatched bounds or cornerRadius (the candidate isn't from the same element)
|
||||
//
|
||||
// False-match risk: two unrelated elements with bit-identical bounds and corner radii.
|
||||
// Requires geometric coincidence (rare in practice), and even when it fires, the misattributed
|
||||
// outline still lands at the correct screen position with the correct color — the pixels
|
||||
// match the unmerged ground truth for opaque borders (the only kind we merge).
|
||||
//INTERNAL
|
||||
try_dispatch_clay_border_merge :: proc(bounds: Rectangle, border_data: clay.BorderRenderData) -> bool {
|
||||
border_width := border_data.width
|
||||
uniform_nonzero :=
|
||||
border_width.left == border_width.top &&
|
||||
border_width.top == border_width.right &&
|
||||
border_width.right == border_width.bottom &&
|
||||
border_width.top > 0
|
||||
if !uniform_nonzero do return false
|
||||
if border_data.color[3] < 255 do return false
|
||||
|
||||
for len(GLOB.clay_merge_open_stack) > 0 {
|
||||
candidate := pop(&GLOB.clay_merge_open_stack)
|
||||
if candidate.outer_bounds != bounds do continue
|
||||
if candidate.corner_radii != border_data.cornerRadius do continue
|
||||
apply_clay_border_merge_to_primitive(candidate, border_data)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Mutates `tmp_primitives[candidate.primitive_index]` in place: shrinks the SDF shape by
|
||||
// the uniform border width so the (outward) outline lands at the outer bounds, sets the
|
||||
// outline flag and params, and — for `Fill_Texture` candidates — refits the texture's UV
|
||||
// against `inner_bounds` so the image doesn't overflow into the border strip.
|
||||
//
|
||||
// The primitive's `bounds` field stays at the outer bounds: the rasterized quad already
|
||||
// covers the area the outline now occupies. Skipping the bounds expansion that
|
||||
// `apply_brush_and_outline` would normally do is intentional — expanding here would push the
|
||||
// rasterized quad past Clay's outer edge.
|
||||
//INTERNAL
|
||||
apply_clay_border_merge_to_primitive :: proc(
|
||||
candidate: Clay_Merge_Candidate,
|
||||
border_data: clay.BorderRenderData,
|
||||
) {
|
||||
prim := &GLOB.tmp_primitives[candidate.primitive_index]
|
||||
uniform_width := f32(border_data.width.top)
|
||||
dpi_scale := GLOB.dpi_scaling
|
||||
|
||||
inner_half_width := candidate.outer_bounds.width * 0.5 - uniform_width
|
||||
inner_half_height := candidate.outer_bounds.height * 0.5 - uniform_width
|
||||
prim.params.rrect.half_size_ppx = {inner_half_width * dpi_scale, inner_half_height * dpi_scale}
|
||||
prim.params.rrect.radii_ppx = {
|
||||
max(0, candidate.corner_radii.topLeft - uniform_width) * dpi_scale,
|
||||
max(0, candidate.corner_radii.topRight - uniform_width) * dpi_scale,
|
||||
max(0, candidate.corner_radii.bottomRight - uniform_width) * dpi_scale,
|
||||
max(0, candidate.corner_radii.bottomLeft - uniform_width) * dpi_scale,
|
||||
}
|
||||
|
||||
// Set the outline bit in the packed flags field (low byte = Shape_Kind, bits 8+ = Shape_Flags).
|
||||
prim.flags |= u32(transmute(u8)Shape_Flags{.Outline}) << 8
|
||||
prim.effects.outline_color = color_from_clay(border_data.color)
|
||||
prim.effects.outline_packed = pack_f16_pair(f16(uniform_width * dpi_scale), 0)
|
||||
|
||||
if candidate.kind == .Fill_Texture {
|
||||
// The candidate was only pushed if its `fit_rect == outer_bounds` at emission time, so the
|
||||
// image fills the rasterized quad. Refit UVs against `inner_bounds` so the image is scoped
|
||||
// to the area inside the new outline rather than overflowing into the border strip.
|
||||
inner_bounds := Rectangle {
|
||||
x = candidate.outer_bounds.x + uniform_width,
|
||||
y = candidate.outer_bounds.y + uniform_width,
|
||||
width = candidate.outer_bounds.width - 2 * uniform_width,
|
||||
height = candidate.outer_bounds.height - 2 * uniform_width,
|
||||
}
|
||||
uv_rect, _, _ := fit_params(candidate.image_data.fit, inner_bounds, candidate.image_data.texture_id)
|
||||
prim.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive.
|
||||
// Extracted from the main `prepare_clay_batch` walk so that the deferred-buffer flush path
|
||||
// can replay commands accumulated during an open backdrop scope without duplicating the
|
||||
@@ -864,30 +1147,61 @@ dispatch_clay_command :: proc(
|
||||
render_data := render_command.renderData.image
|
||||
if render_data.imageData == nil do return
|
||||
img_data := (^Clay_Image_Data)(render_data.imageData)^
|
||||
cr := render_data.cornerRadius
|
||||
corner_radii_clay := render_data.cornerRadius
|
||||
radii := Rectangle_Radii {
|
||||
top_left = cr.topLeft,
|
||||
top_right = cr.topRight,
|
||||
bottom_right = cr.bottomRight,
|
||||
bottom_left = cr.bottomLeft,
|
||||
top_left = corner_radii_clay.topLeft,
|
||||
top_right = corner_radii_clay.topRight,
|
||||
bottom_right = corner_radii_clay.bottomRight,
|
||||
bottom_left = corner_radii_clay.bottomLeft,
|
||||
}
|
||||
|
||||
// Background color behind the image (Clay allows it)
|
||||
bg := color_from_clay(render_data.backgroundColor)
|
||||
if bg.a > 0 {
|
||||
rectangle(layer, bounds, bg, radii = radii)
|
||||
}
|
||||
background_color := color_from_clay(render_data.backgroundColor)
|
||||
uv_rect, sampler, fit_rect := fit_params(img_data.fit, bounds, img_data.texture_id)
|
||||
|
||||
// Compute fit UVs
|
||||
uv, sampler, inner := fit_params(img_data.fit, bounds, img_data.texture_id)
|
||||
|
||||
// Draw the image
|
||||
if background_color.a > 0 {
|
||||
// Bg behind image. Push the bg primitive as the merge candidate so a matching Border
|
||||
// turns into a bg+border-merged primitive plus a separate image draw on top.
|
||||
rectangle(layer, bounds, background_color, radii = radii)
|
||||
bg_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
||||
rectangle(
|
||||
layer,
|
||||
inner,
|
||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv, sampler = sampler},
|
||||
fit_rect,
|
||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
|
||||
radii = radii,
|
||||
)
|
||||
append(
|
||||
&GLOB.clay_merge_open_stack,
|
||||
Clay_Merge_Candidate {
|
||||
primitive_index = bg_primitive_index,
|
||||
outer_bounds = bounds,
|
||||
corner_radii = corner_radii_clay,
|
||||
kind = .Fill_Color,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// No bg: the image itself can host the outline if its fit fully covers Clay's bounds.
|
||||
// `Fit_Mode.Fit` with aspect mismatch returns a sub-rect, which can't host an outline
|
||||
// (the rasterized quad wouldn't reach Clay's outer edge), so we skip pushing.
|
||||
rectangle(
|
||||
layer,
|
||||
fit_rect,
|
||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
|
||||
radii = radii,
|
||||
)
|
||||
if fit_rect == bounds {
|
||||
img_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
||||
append(
|
||||
&GLOB.clay_merge_open_stack,
|
||||
Clay_Merge_Candidate {
|
||||
primitive_index = img_primitive_index,
|
||||
outer_bounds = bounds,
|
||||
corner_radii = corner_radii_clay,
|
||||
image_data = img_data,
|
||||
kind = .Fill_Texture,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
case clay.RenderCommandType.ScissorStart:
|
||||
if bounds.width == 0 || bounds.height == 0 do return
|
||||
|
||||
@@ -919,29 +1233,38 @@ dispatch_clay_command :: proc(
|
||||
unimplemented("Clay overlays not supported yet...")
|
||||
case clay.RenderCommandType.Rectangle:
|
||||
render_data := render_command.renderData.rectangle
|
||||
cr := render_data.cornerRadius
|
||||
color := color_from_clay(render_data.backgroundColor)
|
||||
corner_radii_clay := render_data.cornerRadius
|
||||
background_color := color_from_clay(render_data.backgroundColor)
|
||||
radii := Rectangle_Radii {
|
||||
top_left = cr.topLeft,
|
||||
top_right = cr.topRight,
|
||||
bottom_right = cr.bottomRight,
|
||||
bottom_left = cr.bottomLeft,
|
||||
top_left = corner_radii_clay.topLeft,
|
||||
top_right = corner_radii_clay.topRight,
|
||||
bottom_right = corner_radii_clay.bottomRight,
|
||||
bottom_left = corner_radii_clay.bottomLeft,
|
||||
}
|
||||
|
||||
rectangle(layer, bounds, color, radii = radii)
|
||||
rectangle(layer, bounds, background_color, radii = radii)
|
||||
// Register this primitive as a merge candidate. If the element has a matching Border
|
||||
// later in the stream (after its children's commands), `try_dispatch_clay_border_merge`
|
||||
// will pop this candidate and mutate the primitive in-place to add the outline.
|
||||
primitive_index := u32(len(GLOB.tmp_primitives) - 1)
|
||||
append(
|
||||
&GLOB.clay_merge_open_stack,
|
||||
Clay_Merge_Candidate {
|
||||
primitive_index = primitive_index,
|
||||
outer_bounds = bounds,
|
||||
corner_radii = corner_radii_clay,
|
||||
kind = .Fill_Color,
|
||||
},
|
||||
)
|
||||
case clay.RenderCommandType.Border:
|
||||
render_data := render_command.renderData.border
|
||||
cr := render_data.cornerRadius
|
||||
color := color_from_clay(render_data.color)
|
||||
thickness := f32(render_data.width.top)
|
||||
radii := Rectangle_Radii {
|
||||
top_left = cr.topLeft,
|
||||
top_right = cr.topRight,
|
||||
bottom_right = cr.bottomRight,
|
||||
bottom_left = cr.bottomLeft,
|
||||
}
|
||||
|
||||
rectangle(layer, bounds, BLANK, outline_color = color, outline_width = thickness, radii = radii)
|
||||
if try_dispatch_clay_border_merge(bounds, render_data) do return
|
||||
clay_emit_partial_border(
|
||||
layer,
|
||||
bounds,
|
||||
color_from_clay(render_data.color),
|
||||
render_data.width,
|
||||
render_data.cornerRadius,
|
||||
)
|
||||
case clay.RenderCommandType.Custom:
|
||||
// Copy the CustomRenderData by value so we can patch its `customData` field for the
|
||||
// user callback without mutating Clay-owned memory. After unwrapping, the callback
|
||||
@@ -1021,6 +1344,10 @@ flush_deferred_and_close_backdrop_scope :: proc(
|
||||
end_backdrop(layer)
|
||||
backdrop_scope_open^ = false
|
||||
}
|
||||
// Clear the merge stack at scope/stratum boundaries: any pending candidates from the
|
||||
// pre-scope (or pre-transition) commands stay as plain primitives — they can't merge
|
||||
// with Borders on the far side of the boundary because that would change draw order.
|
||||
clear(&GLOB.clay_merge_open_stack)
|
||||
for index in deferred_indices^ {
|
||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, index)
|
||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
||||
@@ -1069,6 +1396,10 @@ prepare_clay_batch :: proc(
|
||||
// the previous batch already saw.
|
||||
previous_z_index := GLOB.clay_z_index
|
||||
|
||||
// Start with a clean merge stack. The stack is also cleared by
|
||||
// `flush_deferred_and_close_backdrop_scope` at every stratum boundary; both clears together
|
||||
// ensure merge candidates never pair across a boundary that would shift draw order.
|
||||
clear(&GLOB.clay_merge_open_stack)
|
||||
for i in 0 ..< command_count {
|
||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, i32(i))
|
||||
|
||||
@@ -1098,6 +1429,9 @@ prepare_clay_batch :: proc(
|
||||
} else if backdrop_scope_open {
|
||||
append(&deferred_indices, i32(i))
|
||||
} else {
|
||||
// Rectangle/Image dispatches push merge candidates; Border dispatches pop the stack
|
||||
// to retroactively add an outline to a matching candidate. See
|
||||
// `try_dispatch_clay_border_merge` for the matching semantics.
|
||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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