Added full clay border support to draw #28
@@ -75,6 +75,11 @@
|
|||||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures",
|
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- textures",
|
||||||
"cwd": "$ZED_WORKTREE_ROOT",
|
"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",
|
"label": "Run draw gaussian-blur example",
|
||||||
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
|
"command": "odin run draw/examples -debug -out=out/debug/draw-examples -- gaussian-blur",
|
||||||
|
|||||||
+806
@@ -0,0 +1,806 @@
|
|||||||
|
// Clay UI integration for the `draw` package.
|
||||||
|
//
|
||||||
|
// All code in this file is dedicated to bridging Clay's render command stream into `draw`'s
|
||||||
|
// primitive/sub-batch pipeline. Nothing outside this file should reference the `clay` package
|
||||||
|
// directly; everything Clay-related (types, lifecycle helpers, render-command dispatch, the
|
||||||
|
// border-merge stack, the Clay backdrop bracket walker, the text measure/error callbacks,
|
||||||
|
// and the `Clay_Image_Data` user-facing helper) lives here. `draw.odin`'s lifecycle procs
|
||||||
|
// call `init_clay`, `destroy_clay`, and `clear_clay_per_frame` to drive the bits of state
|
||||||
|
// that necessarily live on the shared `Global` struct.
|
||||||
|
package draw
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
|
import "core:c"
|
||||||
|
import "core:log"
|
||||||
|
import "core:strings"
|
||||||
|
import sdl "vendor:sdl3"
|
||||||
|
import sdl_ttf "vendor:sdl3/ttf"
|
||||||
|
|
||||||
|
import clay "../vendor/clay"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Lifecycle ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Allocate the Clay arena, build the merge-candidate stack, hand the arena to Clay, and
|
||||||
|
// register the text-measurement and error callbacks. Called by `init` once `GLOB` has been
|
||||||
|
// populated with the device/window state Clay's callbacks read from.
|
||||||
|
//INTERNAL
|
||||||
|
init_clay :: proc(window: ^sdl.Window, allocator: runtime.Allocator) {
|
||||||
|
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
|
||||||
|
GLOB.clay_merge_open_stack = make([dynamic]Clay_Merge_Candidate, 0, 16, allocator = allocator)
|
||||||
|
GLOB.clay_memory = make([^]u8, min_memory_size, allocator = allocator)
|
||||||
|
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
|
||||||
|
window_width, window_height: c.int
|
||||||
|
sdl.GetWindowSize(window, &window_width, &window_height)
|
||||||
|
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
||||||
|
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the Clay arena memory allocated in `init_clay`. Called by `destroy`. The merge stack
|
||||||
|
// is left to the package allocator's normal teardown to preserve historical behavior.
|
||||||
|
//INTERNAL
|
||||||
|
destroy_clay :: proc(allocator: runtime.Allocator) {
|
||||||
|
free(GLOB.clay_memory, allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset Clay per-frame state: the z-index high-water mark and the border-merge stack.
|
||||||
|
// Called by `clear_global` at the start of every frame.
|
||||||
|
//INTERNAL
|
||||||
|
clear_clay_per_frame :: proc() {
|
||||||
|
GLOB.clay_z_index = 0
|
||||||
|
clear(&GLOB.clay_merge_open_stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Image data (Clay RenderCommandType.Image payload) ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Clay_Image_Data :: struct {
|
||||||
|
texture_id: Texture_Id,
|
||||||
|
fit: Fit_Mode,
|
||||||
|
tint: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
|
||||||
|
return {texture_id = id, fit = fit, tint = tint}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Callbacks (clay -> draw) ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
||||||
|
context = GLOB.odin_context
|
||||||
|
log.error("Clay error:", errorData.errorType, errorData.errorText)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
measure_text_clay :: proc "c" (
|
||||||
|
text: clay.StringSlice,
|
||||||
|
config: ^clay.TextElementConfig,
|
||||||
|
user_data: rawptr,
|
||||||
|
) -> clay.Dimensions {
|
||||||
|
context = GLOB.odin_context
|
||||||
|
text := string(text.chars[:text.length])
|
||||||
|
c_text := strings.clone_to_cstring(text, context.temp_allocator)
|
||||||
|
defer delete(c_text, context.temp_allocator)
|
||||||
|
width, height: c.int
|
||||||
|
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
|
||||||
|
log.panicf("Failed to measure text: %s", sdl.GetError())
|
||||||
|
}
|
||||||
|
|
||||||
|
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Custom draw + customData envelope ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Called for each Clay `RenderCommandType.Custom` render command that
|
||||||
|
// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant
|
||||||
|
// (e.g. `Backdrop_Marker`).
|
||||||
|
//
|
||||||
|
// - `layer` is the layer the command belongs to (post-z-index promotion).
|
||||||
|
// - `bounds` is already translated into the active layer's coordinate system
|
||||||
|
// and pre-DPI, matching what the built-in shape procs expect.
|
||||||
|
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
||||||
|
// `backgroundColor` and `cornerRadius`. Its `customData` field has been
|
||||||
|
// unwrapped from the `Clay_Custom` envelope: it points at the user's own
|
||||||
|
// data (the value the user wrote into the `rawptr` variant), not at the
|
||||||
|
// `Clay_Custom` itself. If the union was zero-init (no variant set) or
|
||||||
|
// `customData` was originally nil, the callback receives nil.
|
||||||
|
//
|
||||||
|
// The callback must not call `new_layer` or `prepare_clay_batch`.
|
||||||
|
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
||||||
|
|
||||||
|
ClayBatch :: struct {
|
||||||
|
bounds: Rectangle,
|
||||||
|
cmds: clay.ClayArray(clay.RenderCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
|
||||||
|
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
|
||||||
|
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
|
||||||
|
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
|
||||||
|
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
|
||||||
|
// exactly as if no wrapper were involved.
|
||||||
|
//
|
||||||
|
// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom`
|
||||||
|
// value. The user owns that storage; its lifetime must span the Clay layout call and the
|
||||||
|
// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates
|
||||||
|
// the contract — the dispatcher will read its first bytes as a union tag and either route
|
||||||
|
// the draw incorrectly or panic on type assertion. There is no recovery path; this is a
|
||||||
|
// strict-discipline API by design.
|
||||||
|
//
|
||||||
|
// Construction notes (Odin implicit-conversion rules):
|
||||||
|
// - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly.
|
||||||
|
// Variant-to-union conversion is implicit.
|
||||||
|
// - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is
|
||||||
|
// required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare
|
||||||
|
// `up: Clay_Custom = &my_struct` is a compile error.
|
||||||
|
Clay_Custom :: union {
|
||||||
|
Backdrop_Marker,
|
||||||
|
rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-primitive parameters for a backdrop blur dispatched through the Clay integration.
|
||||||
|
// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream,
|
||||||
|
// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to
|
||||||
|
// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no
|
||||||
|
// in-band magic field needed (compiler-enforced).
|
||||||
|
Backdrop_Marker :: struct {
|
||||||
|
sigma: f32,
|
||||||
|
tint: Color,
|
||||||
|
radii: Rectangle_Radii,
|
||||||
|
feather_ppx: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Border-merge stack ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if cmd.commandType != .Custom do return false
|
||||||
|
p := cmd.renderData.custom.customData
|
||||||
|
if p == nil do return false
|
||||||
|
_, ok := (^Clay_Custom)(p).(Backdrop_Marker)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Border emission ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 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(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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Command dispatch ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// per-command lowering code.
|
||||||
|
//INTERNAL
|
||||||
|
dispatch_clay_command :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
render_command: ^clay.RenderCommand,
|
||||||
|
custom_draw: Custom_Draw,
|
||||||
|
temp_allocator: runtime.Allocator,
|
||||||
|
) {
|
||||||
|
// Translate bounding box of the primitive by the layer position
|
||||||
|
bounds := Rectangle {
|
||||||
|
x = render_command.boundingBox.x + layer.bounds.x,
|
||||||
|
y = render_command.boundingBox.y + layer.bounds.y,
|
||||||
|
width = render_command.boundingBox.width,
|
||||||
|
height = render_command.boundingBox.height,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch render_command.commandType {
|
||||||
|
case clay.RenderCommandType.None:
|
||||||
|
log.errorf(
|
||||||
|
"Received render command with type None. This generally means we're in some kind of fucked up state.",
|
||||||
|
)
|
||||||
|
case clay.RenderCommandType.Text:
|
||||||
|
render_data := render_command.renderData.text
|
||||||
|
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
||||||
|
c_text := strings.clone_to_cstring(txt, temp_allocator)
|
||||||
|
defer delete(c_text, temp_allocator)
|
||||||
|
// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
|
||||||
|
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
|
||||||
|
sdl_text := cache_get_or_update(
|
||||||
|
Cache_Key{render_command.id, .Clay},
|
||||||
|
c_text,
|
||||||
|
get_font(render_data.fontId, render_data.fontSize),
|
||||||
|
)
|
||||||
|
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, Color(render_data.textColor)})
|
||||||
|
case clay.RenderCommandType.Image:
|
||||||
|
// Any texture
|
||||||
|
render_data := render_command.renderData.image
|
||||||
|
if render_data.imageData == nil do return
|
||||||
|
img_data := (^Clay_Image_Data)(render_data.imageData)^
|
||||||
|
corner_radii_clay := render_data.cornerRadius
|
||||||
|
radii := Rectangle_Radii {
|
||||||
|
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 := Color(render_data.backgroundColor)
|
||||||
|
uv_rect, sampler, fit_rect := fit_params(img_data.fit, bounds, img_data.texture_id)
|
||||||
|
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
|
||||||
|
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
||||||
|
|
||||||
|
if curr_scissor.sub_batch_len != 0 {
|
||||||
|
// Scissor has some content, need to make a new scissor
|
||||||
|
new := Scissor {
|
||||||
|
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
|
||||||
|
bounds = sdl.Rect {
|
||||||
|
c.int(bounds.x * GLOB.dpi_scaling),
|
||||||
|
c.int(bounds.y * GLOB.dpi_scaling),
|
||||||
|
c.int(bounds.width * GLOB.dpi_scaling),
|
||||||
|
c.int(bounds.height * GLOB.dpi_scaling),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
append(&GLOB.scissors, new)
|
||||||
|
layer.scissor_len += 1
|
||||||
|
} else {
|
||||||
|
curr_scissor.bounds = sdl.Rect {
|
||||||
|
c.int(bounds.x * GLOB.dpi_scaling),
|
||||||
|
c.int(bounds.y * GLOB.dpi_scaling),
|
||||||
|
c.int(bounds.width * GLOB.dpi_scaling),
|
||||||
|
c.int(bounds.height * GLOB.dpi_scaling),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case clay.RenderCommandType.ScissorEnd:
|
||||||
|
case clay.RenderCommandType.OverlayColorStart, clay.RenderCommandType.OverlayColorEnd:
|
||||||
|
unimplemented("Clay overlays not supported yet...")
|
||||||
|
case clay.RenderCommandType.Rectangle:
|
||||||
|
render_data := render_command.renderData.rectangle
|
||||||
|
corner_radii_clay := render_data.cornerRadius
|
||||||
|
background_color := Color(render_data.backgroundColor)
|
||||||
|
radii := Rectangle_Radii {
|
||||||
|
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, 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
|
||||||
|
if try_dispatch_clay_border_merge(bounds, render_data) do return
|
||||||
|
clay_emit_partial_border(
|
||||||
|
layer,
|
||||||
|
bounds,
|
||||||
|
Color(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
|
||||||
|
// sees its own pointer in `render_data.customData`, identical to what it would see
|
||||||
|
// if `Clay_Custom` did not exist as an intermediary.
|
||||||
|
patched := render_command.renderData.custom
|
||||||
|
// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
|
||||||
|
// `customData` both surface to the callback as `customData = nil`.
|
||||||
|
patched.customData = nil
|
||||||
|
if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
|
||||||
|
switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
|
||||||
|
case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
||||||
|
// them here; reaching this branch means either the walker logic is broken or the
|
||||||
|
// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
|
||||||
|
// and this re-check (heap corruption / lifetime bug in user-managed customData
|
||||||
|
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
|
||||||
|
// silently-dropped panel.
|
||||||
|
log.panicf(
|
||||||
|
"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
|
||||||
|
render_command.renderData.custom.customData,
|
||||||
|
)
|
||||||
|
case rawptr: patched.customData = custom_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if custom_draw != nil {
|
||||||
|
custom_draw(layer, bounds, patched)
|
||||||
|
} else if patched.customData != nil {
|
||||||
|
log.panicf(
|
||||||
|
"Received clay render command of type custom with non-nil user data but no custom_draw proc provided.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
|
||||||
|
// Caller guarantees:
|
||||||
|
// - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch`
|
||||||
|
// contract assertion is satisfied;
|
||||||
|
// - the command's `customData` points at a `Clay_Custom` whose active variant is
|
||||||
|
// `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`).
|
||||||
|
//INTERNAL
|
||||||
|
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
||||||
|
bounds := Rectangle {
|
||||||
|
x = cmd.boundingBox.x + layer.bounds.x,
|
||||||
|
y = cmd.boundingBox.y + layer.bounds.y,
|
||||||
|
width = cmd.boundingBox.width,
|
||||||
|
height = cmd.boundingBox.height,
|
||||||
|
}
|
||||||
|
// Type-asserting form (no `, ok`): panics loudly if the variant tag changed since
|
||||||
|
// `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in
|
||||||
|
// user-managed customData.
|
||||||
|
marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker)
|
||||||
|
backdrop_blur(
|
||||||
|
layer,
|
||||||
|
bounds,
|
||||||
|
gaussian_sigma = marker.sigma,
|
||||||
|
tint = marker.tint,
|
||||||
|
radii = marker.radii,
|
||||||
|
feather_ppx = marker.feather_ppx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the in-flight backdrop scope (if open) and replay every command accumulated in the
|
||||||
|
// deferred index buffer. Ordering: end_backdrop first so deferred non-backdrop draws land
|
||||||
|
// at submission position relative to the bracket they followed (the bracket is now closed,
|
||||||
|
// so these draws render after it). Used at every zIndex transition and at end of stream.
|
||||||
|
//INTERNAL
|
||||||
|
flush_deferred_and_close_backdrop_scope :: proc(
|
||||||
|
layer: ^Layer,
|
||||||
|
batch: ^ClayBatch,
|
||||||
|
deferred_indices: ^[dynamic]i32,
|
||||||
|
backdrop_scope_open: ^bool,
|
||||||
|
custom_draw: Custom_Draw,
|
||||||
|
temp_allocator: runtime.Allocator,
|
||||||
|
) {
|
||||||
|
if backdrop_scope_open^ {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
clear(deferred_indices)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
// ----- Main entry point ------------
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Process Clay render commands into shape, text, and backdrop primitives.
|
||||||
|
//
|
||||||
|
// Single-walk dispatcher with a deferred buffer. The walk does three things per command:
|
||||||
|
// 1. zIndex transitions: close the in-flight scope, flush any deferred non-backdrop
|
||||||
|
// commands into the current layer, then open a new layer seeded with `base_layer.bounds`
|
||||||
|
// (NOT the bumping element's bounds — Clay's floating elements with `clipTo = .None`
|
||||||
|
// should not be over-clipped, and `clipTo = .AttachedParent` floating elements get a
|
||||||
|
// Clay-emitted ScissorStart immediately afterward that narrows correctly).
|
||||||
|
// 2. Backdrop commands: open a scope on first encounter (extending it on subsequent ones),
|
||||||
|
// then dispatch the backdrop_blur call.
|
||||||
|
// 3. Non-backdrop commands during an open scope: append to the deferred buffer for replay
|
||||||
|
// after the scope closes. The buffer holds command indices, not pointers, so it stays
|
||||||
|
// valid even if the underlying ClayArray reallocates.
|
||||||
|
// At end of stream, flush whatever remains.
|
||||||
|
prepare_clay_batch :: proc(
|
||||||
|
base_layer: ^Layer,
|
||||||
|
batch: ^ClayBatch,
|
||||||
|
mouse_wheel_delta: [2]f32,
|
||||||
|
frame_time: f32 = 0,
|
||||||
|
custom_draw: Custom_Draw = nil,
|
||||||
|
temp_allocator := context.temp_allocator,
|
||||||
|
) {
|
||||||
|
mouse_pos: [2]f32
|
||||||
|
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
|
||||||
|
|
||||||
|
// Update clay internals
|
||||||
|
clay.SetPointerState(
|
||||||
|
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
|
||||||
|
.LEFT in mouse_flags,
|
||||||
|
)
|
||||||
|
clay.UpdateScrollContainers(true, mouse_wheel_delta, frame_time)
|
||||||
|
|
||||||
|
layer := base_layer
|
||||||
|
command_count := int(batch.cmds.length)
|
||||||
|
deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
|
||||||
|
backdrop_scope_open := false
|
||||||
|
// Seed from GLOB.clay_z_index so multi-batch frames preserve the original semantics: a
|
||||||
|
// later call to `prepare_clay_batch` doesn't re-trigger layer splits for zIndex values
|
||||||
|
// 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))
|
||||||
|
|
||||||
|
// zIndex transition: close out current stratum, create new layer, continue.
|
||||||
|
if cmd.zIndex > previous_z_index {
|
||||||
|
log.debug("Higher zIndex found, creating new layer & setting z_index to", cmd.zIndex)
|
||||||
|
flush_deferred_and_close_backdrop_scope(
|
||||||
|
layer,
|
||||||
|
batch,
|
||||||
|
&deferred_indices,
|
||||||
|
&backdrop_scope_open,
|
||||||
|
custom_draw,
|
||||||
|
temp_allocator,
|
||||||
|
)
|
||||||
|
layer = new_layer(layer, base_layer.bounds)
|
||||||
|
previous_z_index = cmd.zIndex
|
||||||
|
// Keep GLOB.clay_z_index in sync for any external readers (debug tooling, etc.).
|
||||||
|
GLOB.clay_z_index = cmd.zIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_clay_backdrop(cmd) {
|
||||||
|
if !backdrop_scope_open {
|
||||||
|
begin_backdrop(layer)
|
||||||
|
backdrop_scope_open = true
|
||||||
|
}
|
||||||
|
dispatch_clay_backdrop(layer, cmd)
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End-of-stream: flush whatever remains.
|
||||||
|
flush_deferred_and_close_backdrop_scope(
|
||||||
|
layer,
|
||||||
|
batch,
|
||||||
|
&deferred_indices,
|
||||||
|
&backdrop_scope_open,
|
||||||
|
custom_draw,
|
||||||
|
temp_allocator,
|
||||||
|
)
|
||||||
|
}
|
||||||
+4
-407
@@ -67,12 +67,9 @@ import "base:runtime"
|
|||||||
import "core:c"
|
import "core:c"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:math"
|
import "core:math"
|
||||||
import "core:strings"
|
|
||||||
import sdl "vendor:sdl3"
|
import sdl "vendor:sdl3"
|
||||||
import sdl_ttf "vendor:sdl3/ttf"
|
import sdl_ttf "vendor:sdl3/ttf"
|
||||||
|
|
||||||
import clay "../vendor/clay"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Shader format ------------
|
// ----- Shader format ------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -171,6 +168,7 @@ Global :: struct {
|
|||||||
|
|
||||||
// -- Clay (once per frame in prepare_clay_batch) --
|
// -- Clay (once per frame in prepare_clay_batch) --
|
||||||
clay_memory: [^]u8, // Raw memory block backing Clay's internal arena.
|
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 (occasional — font registration and text cache lookups) --
|
||||||
text_cache: Text_Cache, // Font registry, SDL_ttf engine, and cached TTF_Text objects.
|
text_cache: Text_Cache, // Font registry, SDL_ttf engine, and cached TTF_Text objects.
|
||||||
@@ -266,11 +264,6 @@ Brush :: union {
|
|||||||
Texture_Fill,
|
Texture_Fill,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert clay.Color ([4]c.float in 0–255 range) to Color.
|
|
||||||
color_from_clay :: #force_inline proc(clay_color: clay.Color) -> Color {
|
|
||||||
return Color{u8(clay_color[0]), u8(clay_color[1]), u8(clay_color[2]), u8(clay_color[3])}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert Color to [4]f32 in 0.0–1.0 range. Useful for SDL interop (e.g. clear color).
|
// Convert Color to [4]f32 in 0.0–1.0 range. Useful for SDL interop (e.g. clear color).
|
||||||
color_to_f32 :: proc(color: Color) -> [4]f32 {
|
color_to_f32 :: proc(color: Color) -> [4]f32 {
|
||||||
INV :: 1.0 / 255.0
|
INV :: 1.0 / 255.0
|
||||||
@@ -346,8 +339,6 @@ init :: proc(
|
|||||||
) -> (
|
) -> (
|
||||||
ok: bool,
|
ok: bool,
|
||||||
) {
|
) {
|
||||||
min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
|
|
||||||
|
|
||||||
core, core_ok := create_core_2d(device, window)
|
core, core_ok := create_core_2d(device, window)
|
||||||
if !core_ok {
|
if !core_ok {
|
||||||
return false
|
return false
|
||||||
@@ -394,7 +385,6 @@ init :: proc(
|
|||||||
pending_text_releases = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
pending_text_releases = make([dynamic]^sdl_ttf.Text, 0, 16, allocator = allocator),
|
||||||
odin_context = odin_context,
|
odin_context = odin_context,
|
||||||
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
dpi_scaling = sdl.GetWindowDisplayScale(window),
|
||||||
clay_memory = make([^]u8, min_memory_size, allocator = allocator),
|
|
||||||
core_2d = core,
|
core_2d = core,
|
||||||
backdrop = backdrop,
|
backdrop = backdrop,
|
||||||
text_cache = text_cache,
|
text_cache = text_cache,
|
||||||
@@ -403,12 +393,7 @@ init :: proc(
|
|||||||
// Reserve slot 0 for INVALID_TEXTURE
|
// Reserve slot 0 for INVALID_TEXTURE
|
||||||
append(&GLOB.texture_slots, Texture_Slot{})
|
append(&GLOB.texture_slots, Texture_Slot{})
|
||||||
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
|
log.debug("Window DPI scaling:", GLOB.dpi_scaling)
|
||||||
arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
|
init_clay(window, allocator)
|
||||||
window_width, window_height: c.int
|
|
||||||
sdl.GetWindowSize(window, &window_width, &window_height)
|
|
||||||
|
|
||||||
clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
|
|
||||||
clay.SetMeasureTextFunction(measure_text_clay, nil)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +432,7 @@ destroy :: proc(device: ^sdl.GPUDevice, allocator := context.allocator) {
|
|||||||
delete(GLOB.tmp_gaussian_blur_primitives)
|
delete(GLOB.tmp_gaussian_blur_primitives)
|
||||||
for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
|
for ttf_text in GLOB.tmp_uncached_text do sdl_ttf.DestroyText(ttf_text)
|
||||||
delete(GLOB.tmp_uncached_text)
|
delete(GLOB.tmp_uncached_text)
|
||||||
free(GLOB.clay_memory, allocator)
|
destroy_clay(allocator)
|
||||||
process_pending_texture_releases()
|
process_pending_texture_releases()
|
||||||
destroy_all_textures()
|
destroy_all_textures()
|
||||||
destroy_sampler_pool()
|
destroy_sampler_pool()
|
||||||
@@ -467,7 +452,6 @@ clear_global :: proc() {
|
|||||||
clear(&GLOB.pending_text_releases)
|
clear(&GLOB.pending_text_releases)
|
||||||
|
|
||||||
GLOB.curr_layer_index = 0
|
GLOB.curr_layer_index = 0
|
||||||
GLOB.clay_z_index = 0
|
|
||||||
GLOB.cleared = false
|
GLOB.cleared = false
|
||||||
GLOB.open_backdrop_layer = nil
|
GLOB.open_backdrop_layer = nil
|
||||||
// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
|
// Destroy uncached TTF_Text objects from the previous frame (after end() has submitted draw data)
|
||||||
@@ -482,6 +466,7 @@ clear_global :: proc() {
|
|||||||
clear(&GLOB.tmp_primitives)
|
clear(&GLOB.tmp_primitives)
|
||||||
clear(&GLOB.tmp_sub_batches)
|
clear(&GLOB.tmp_sub_batches)
|
||||||
clear(&GLOB.tmp_gaussian_blur_primitives)
|
clear(&GLOB.tmp_gaussian_blur_primitives)
|
||||||
|
clear_clay_per_frame()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -725,394 +710,6 @@ append_or_extend_sub_batch :: proc(
|
|||||||
layer.sub_batch_len += 1
|
layer.sub_batch_len += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
// ----- Clay ------------
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
|
|
||||||
context = GLOB.odin_context
|
|
||||||
log.error("Clay error:", errorData.errorType, errorData.errorText)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(private = "file")
|
|
||||||
measure_text_clay :: proc "c" (
|
|
||||||
text: clay.StringSlice,
|
|
||||||
config: ^clay.TextElementConfig,
|
|
||||||
user_data: rawptr,
|
|
||||||
) -> clay.Dimensions {
|
|
||||||
context = GLOB.odin_context
|
|
||||||
text := string(text.chars[:text.length])
|
|
||||||
c_text := strings.clone_to_cstring(text, context.temp_allocator)
|
|
||||||
defer delete(c_text, context.temp_allocator)
|
|
||||||
width, height: c.int
|
|
||||||
if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
|
|
||||||
log.panicf("Failed to measure text: %s", sdl.GetError())
|
|
||||||
}
|
|
||||||
|
|
||||||
return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called for each Clay `RenderCommandType.Custom` render command that
|
|
||||||
// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant
|
|
||||||
// (e.g. `Backdrop_Marker`).
|
|
||||||
//
|
|
||||||
// - `layer` is the layer the command belongs to (post-z-index promotion).
|
|
||||||
// - `bounds` is already translated into the active layer's coordinate system
|
|
||||||
// and pre-DPI, matching what the built-in shape procs expect.
|
|
||||||
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
|
|
||||||
// `backgroundColor` and `cornerRadius`. Its `customData` field has been
|
|
||||||
// unwrapped from the `Clay_Custom` envelope: it points at the user's own
|
|
||||||
// data (the value the user wrote into the `rawptr` variant), not at the
|
|
||||||
// `Clay_Custom` itself. If the union was zero-init (no variant set) or
|
|
||||||
// `customData` was originally nil, the callback receives nil.
|
|
||||||
//
|
|
||||||
// The callback must not call `new_layer` or `prepare_clay_batch`.
|
|
||||||
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)
|
|
||||||
|
|
||||||
ClayBatch :: struct {
|
|
||||||
bounds: Rectangle,
|
|
||||||
cmds: clay.ClayArray(clay.RenderCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
|
|
||||||
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
|
|
||||||
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
|
|
||||||
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
|
|
||||||
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
|
|
||||||
// exactly as if no wrapper were involved.
|
|
||||||
//
|
|
||||||
// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom`
|
|
||||||
// value. The user owns that storage; its lifetime must span the Clay layout call and the
|
|
||||||
// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates
|
|
||||||
// the contract — the dispatcher will read its first bytes as a union tag and either route
|
|
||||||
// the draw incorrectly or panic on type assertion. There is no recovery path; this is a
|
|
||||||
// strict-discipline API by design.
|
|
||||||
//
|
|
||||||
// Construction notes (Odin implicit-conversion rules):
|
|
||||||
// - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly.
|
|
||||||
// Variant-to-union conversion is implicit.
|
|
||||||
// - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is
|
|
||||||
// required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare
|
|
||||||
// `up: Clay_Custom = &my_struct` is a compile error.
|
|
||||||
Clay_Custom :: union {
|
|
||||||
Backdrop_Marker,
|
|
||||||
rawptr,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-primitive parameters for a backdrop blur dispatched through the Clay integration.
|
|
||||||
// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream,
|
|
||||||
// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to
|
|
||||||
// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no
|
|
||||||
// in-band magic field needed (compiler-enforced).
|
|
||||||
Backdrop_Marker :: struct {
|
|
||||||
sigma: f32,
|
|
||||||
tint: Color,
|
|
||||||
radii: Rectangle_Radii,
|
|
||||||
feather_ppx: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
if cmd.commandType != .Custom do return false
|
|
||||||
p := cmd.renderData.custom.customData
|
|
||||||
if p == nil do return false
|
|
||||||
_, ok := (^Clay_Custom)(p).(Backdrop_Marker)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// per-command lowering code.
|
|
||||||
//INTERNAL
|
|
||||||
dispatch_clay_command :: proc(
|
|
||||||
layer: ^Layer,
|
|
||||||
render_command: ^clay.RenderCommand,
|
|
||||||
custom_draw: Custom_Draw,
|
|
||||||
temp_allocator: runtime.Allocator,
|
|
||||||
) {
|
|
||||||
// Translate bounding box of the primitive by the layer position
|
|
||||||
bounds := Rectangle {
|
|
||||||
x = render_command.boundingBox.x + layer.bounds.x,
|
|
||||||
y = render_command.boundingBox.y + layer.bounds.y,
|
|
||||||
width = render_command.boundingBox.width,
|
|
||||||
height = render_command.boundingBox.height,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch render_command.commandType {
|
|
||||||
case clay.RenderCommandType.None:
|
|
||||||
log.errorf(
|
|
||||||
"Received render command with type None. This generally means we're in some kind of fucked up state.",
|
|
||||||
)
|
|
||||||
case clay.RenderCommandType.Text:
|
|
||||||
render_data := render_command.renderData.text
|
|
||||||
txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
|
|
||||||
c_text := strings.clone_to_cstring(txt, temp_allocator)
|
|
||||||
defer delete(c_text, temp_allocator)
|
|
||||||
// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
|
|
||||||
// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
|
|
||||||
sdl_text := cache_get_or_update(
|
|
||||||
Cache_Key{render_command.id, .Clay},
|
|
||||||
c_text,
|
|
||||||
get_font(render_data.fontId, render_data.fontSize),
|
|
||||||
)
|
|
||||||
prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, color_from_clay(render_data.textColor)})
|
|
||||||
case clay.RenderCommandType.Image:
|
|
||||||
// Any texture
|
|
||||||
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
|
|
||||||
radii := Rectangle_Radii {
|
|
||||||
top_left = cr.topLeft,
|
|
||||||
top_right = cr.topRight,
|
|
||||||
bottom_right = cr.bottomRight,
|
|
||||||
bottom_left = cr.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute fit UVs
|
|
||||||
uv, sampler, inner := fit_params(img_data.fit, bounds, img_data.texture_id)
|
|
||||||
|
|
||||||
// Draw the image
|
|
||||||
rectangle(
|
|
||||||
layer,
|
|
||||||
inner,
|
|
||||||
Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv, sampler = sampler},
|
|
||||||
radii = radii,
|
|
||||||
)
|
|
||||||
case clay.RenderCommandType.ScissorStart:
|
|
||||||
if bounds.width == 0 || bounds.height == 0 do return
|
|
||||||
|
|
||||||
curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]
|
|
||||||
|
|
||||||
if curr_scissor.sub_batch_len != 0 {
|
|
||||||
// Scissor has some content, need to make a new scissor
|
|
||||||
new := Scissor {
|
|
||||||
sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
|
|
||||||
bounds = sdl.Rect {
|
|
||||||
c.int(bounds.x * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.y * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.width * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.height * GLOB.dpi_scaling),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
append(&GLOB.scissors, new)
|
|
||||||
layer.scissor_len += 1
|
|
||||||
} else {
|
|
||||||
curr_scissor.bounds = sdl.Rect {
|
|
||||||
c.int(bounds.x * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.y * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.width * GLOB.dpi_scaling),
|
|
||||||
c.int(bounds.height * GLOB.dpi_scaling),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case clay.RenderCommandType.ScissorEnd:
|
|
||||||
case clay.RenderCommandType.OverlayColorStart, clay.RenderCommandType.OverlayColorEnd:
|
|
||||||
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)
|
|
||||||
radii := Rectangle_Radii {
|
|
||||||
top_left = cr.topLeft,
|
|
||||||
top_right = cr.topRight,
|
|
||||||
bottom_right = cr.bottomRight,
|
|
||||||
bottom_left = cr.bottomLeft,
|
|
||||||
}
|
|
||||||
|
|
||||||
rectangle(layer, bounds, color, radii = radii)
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
// sees its own pointer in `render_data.customData`, identical to what it would see
|
|
||||||
// if `Clay_Custom` did not exist as an intermediary.
|
|
||||||
patched := render_command.renderData.custom
|
|
||||||
// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
|
|
||||||
// `customData` both surface to the callback as `customData = nil`.
|
|
||||||
patched.customData = nil
|
|
||||||
if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
|
|
||||||
switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
|
|
||||||
case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
|
|
||||||
// them here; reaching this branch means either the walker logic is broken or the
|
|
||||||
// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
|
|
||||||
// and this re-check (heap corruption / lifetime bug in user-managed customData
|
|
||||||
// memory). Both are renderer-level bugs that warrant a hard failure rather than a
|
|
||||||
// silently-dropped panel.
|
|
||||||
log.panicf(
|
|
||||||
"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
|
|
||||||
render_command.renderData.custom.customData,
|
|
||||||
)
|
|
||||||
case rawptr: patched.customData = custom_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if custom_draw != nil {
|
|
||||||
custom_draw(layer, bounds, patched)
|
|
||||||
} else if patched.customData != nil {
|
|
||||||
log.panicf(
|
|
||||||
"Received clay render command of type custom with non-nil user data but no custom_draw proc provided.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
|
|
||||||
// Caller guarantees:
|
|
||||||
// - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch`
|
|
||||||
// contract assertion is satisfied;
|
|
||||||
// - the command's `customData` points at a `Clay_Custom` whose active variant is
|
|
||||||
// `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`).
|
|
||||||
//INTERNAL
|
|
||||||
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
|
|
||||||
bounds := Rectangle {
|
|
||||||
x = cmd.boundingBox.x + layer.bounds.x,
|
|
||||||
y = cmd.boundingBox.y + layer.bounds.y,
|
|
||||||
width = cmd.boundingBox.width,
|
|
||||||
height = cmd.boundingBox.height,
|
|
||||||
}
|
|
||||||
// Type-asserting form (no `, ok`): panics loudly if the variant tag changed since
|
|
||||||
// `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in
|
|
||||||
// user-managed customData.
|
|
||||||
marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker)
|
|
||||||
backdrop_blur(
|
|
||||||
layer,
|
|
||||||
bounds,
|
|
||||||
gaussian_sigma = marker.sigma,
|
|
||||||
tint = marker.tint,
|
|
||||||
radii = marker.radii,
|
|
||||||
feather_ppx = marker.feather_ppx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the in-flight backdrop scope (if open) and replay every command accumulated in the
|
|
||||||
// deferred index buffer. Ordering: end_backdrop first so deferred non-backdrop draws land
|
|
||||||
// at submission position relative to the bracket they followed (the bracket is now closed,
|
|
||||||
// so these draws render after it). Used at every zIndex transition and at end of stream.
|
|
||||||
//INTERNAL
|
|
||||||
flush_deferred_and_close_backdrop_scope :: proc(
|
|
||||||
layer: ^Layer,
|
|
||||||
batch: ^ClayBatch,
|
|
||||||
deferred_indices: ^[dynamic]i32,
|
|
||||||
backdrop_scope_open: ^bool,
|
|
||||||
custom_draw: Custom_Draw,
|
|
||||||
temp_allocator: runtime.Allocator,
|
|
||||||
) {
|
|
||||||
if backdrop_scope_open^ {
|
|
||||||
end_backdrop(layer)
|
|
||||||
backdrop_scope_open^ = false
|
|
||||||
}
|
|
||||||
for index in deferred_indices^ {
|
|
||||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, index)
|
|
||||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
|
||||||
}
|
|
||||||
clear(deferred_indices)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process Clay render commands into shape, text, and backdrop primitives.
|
|
||||||
//
|
|
||||||
// Single-walk dispatcher with a deferred buffer. The walk does three things per command:
|
|
||||||
// 1. zIndex transitions: close the in-flight scope, flush any deferred non-backdrop
|
|
||||||
// commands into the current layer, then open a new layer seeded with `base_layer.bounds`
|
|
||||||
// (NOT the bumping element's bounds — Clay's floating elements with `clipTo = .None`
|
|
||||||
// should not be over-clipped, and `clipTo = .AttachedParent` floating elements get a
|
|
||||||
// Clay-emitted ScissorStart immediately afterward that narrows correctly).
|
|
||||||
// 2. Backdrop commands: open a scope on first encounter (extending it on subsequent ones),
|
|
||||||
// then dispatch the backdrop_blur call.
|
|
||||||
// 3. Non-backdrop commands during an open scope: append to the deferred buffer for replay
|
|
||||||
// after the scope closes. The buffer holds command indices, not pointers, so it stays
|
|
||||||
// valid even if the underlying ClayArray reallocates.
|
|
||||||
// At end of stream, flush whatever remains.
|
|
||||||
prepare_clay_batch :: proc(
|
|
||||||
base_layer: ^Layer,
|
|
||||||
batch: ^ClayBatch,
|
|
||||||
mouse_wheel_delta: [2]f32,
|
|
||||||
frame_time: f32 = 0,
|
|
||||||
custom_draw: Custom_Draw = nil,
|
|
||||||
temp_allocator := context.temp_allocator,
|
|
||||||
) {
|
|
||||||
mouse_pos: [2]f32
|
|
||||||
mouse_flags := sdl.GetMouseState(&mouse_pos.x, &mouse_pos.y)
|
|
||||||
|
|
||||||
// Update clay internals
|
|
||||||
clay.SetPointerState(
|
|
||||||
clay.Vector2{mouse_pos.x - base_layer.bounds.x, mouse_pos.y - base_layer.bounds.y},
|
|
||||||
.LEFT in mouse_flags,
|
|
||||||
)
|
|
||||||
clay.UpdateScrollContainers(true, mouse_wheel_delta, frame_time)
|
|
||||||
|
|
||||||
layer := base_layer
|
|
||||||
command_count := int(batch.cmds.length)
|
|
||||||
deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
|
|
||||||
backdrop_scope_open := false
|
|
||||||
// Seed from GLOB.clay_z_index so multi-batch frames preserve the original semantics: a
|
|
||||||
// later call to `prepare_clay_batch` doesn't re-trigger layer splits for zIndex values
|
|
||||||
// the previous batch already saw.
|
|
||||||
previous_z_index := GLOB.clay_z_index
|
|
||||||
|
|
||||||
for i in 0 ..< command_count {
|
|
||||||
cmd := clay.RenderCommandArray_Get(&batch.cmds, i32(i))
|
|
||||||
|
|
||||||
// zIndex transition: close out current stratum, create new layer, continue.
|
|
||||||
if cmd.zIndex > previous_z_index {
|
|
||||||
log.debug("Higher zIndex found, creating new layer & setting z_index to", cmd.zIndex)
|
|
||||||
flush_deferred_and_close_backdrop_scope(
|
|
||||||
layer,
|
|
||||||
batch,
|
|
||||||
&deferred_indices,
|
|
||||||
&backdrop_scope_open,
|
|
||||||
custom_draw,
|
|
||||||
temp_allocator,
|
|
||||||
)
|
|
||||||
layer = new_layer(layer, base_layer.bounds)
|
|
||||||
previous_z_index = cmd.zIndex
|
|
||||||
// Keep GLOB.clay_z_index in sync for any external readers (debug tooling, etc.).
|
|
||||||
GLOB.clay_z_index = cmd.zIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_clay_backdrop(cmd) {
|
|
||||||
if !backdrop_scope_open {
|
|
||||||
begin_backdrop(layer)
|
|
||||||
backdrop_scope_open = true
|
|
||||||
}
|
|
||||||
dispatch_clay_backdrop(layer, cmd)
|
|
||||||
} else if backdrop_scope_open {
|
|
||||||
append(&deferred_indices, i32(i))
|
|
||||||
} else {
|
|
||||||
dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// End-of-stream: flush whatever remains.
|
|
||||||
flush_deferred_and_close_backdrop_scope(
|
|
||||||
layer,
|
|
||||||
batch,
|
|
||||||
&deferred_indices,
|
|
||||||
&backdrop_scope_open,
|
|
||||||
custom_draw,
|
|
||||||
temp_allocator,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Buffer ------------
|
// ----- Buffer ------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
+39
-27
@@ -1,54 +1,71 @@
|
|||||||
package draw_qr
|
package draw_qr
|
||||||
|
|
||||||
|
import "core:mem"
|
||||||
|
import "core:slice"
|
||||||
|
|
||||||
import draw ".."
|
import draw ".."
|
||||||
import "../../qrcode"
|
import "../../qrcode"
|
||||||
|
|
||||||
DFT_QR_DARK :: draw.BLACK // Default QR code dark module color.
|
DFT_QR_DARK :: draw.BLACK // Default QR code dark module color.
|
||||||
DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color.
|
DFT_QR_LIGHT :: draw.WHITE // Default QR code light module color.
|
||||||
DFT_QR_BOOST_ECL :: true // Default QR error correction level boost.
|
DFT_QR_BOOST_ECL :: true // Default QR error correction level boost.
|
||||||
|
DFT_QR_QUIET_ZONE :: 4 // Default light-pixel border on each side; 4 is the QR spec value.
|
||||||
|
|
||||||
// Returns the number of bytes to_texture will write for the given encoded
|
// Returns the number of bytes to_texture will write. Equals dim*dim*4 where
|
||||||
// QR buffer. Equivalent to size*size*4 where size = qrcode.get_size(qrcode_buf).
|
// dim = qrcode.get_size(qrcode_buf) + 2*quiet_zone.
|
||||||
texture_size :: #force_inline proc(qrcode_buf: []u8) -> int {
|
texture_size :: #force_inline proc(qrcode_buf: []u8, quiet_zone: int = DFT_QR_QUIET_ZONE) -> int {
|
||||||
size := qrcode.get_size(qrcode_buf)
|
size := qrcode.get_size(qrcode_buf)
|
||||||
return size * size * 4
|
if size == 0 || quiet_zone < 0 do return 0
|
||||||
|
padded_size := size + 2 * quiet_zone
|
||||||
|
return padded_size * padded_size * 4
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to
|
// Decodes an encoded QR buffer into tightly-packed RGBA pixel data written to
|
||||||
// texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the
|
// texture_buf. No allocations, no GPU calls. Returns the Texture_Desc the
|
||||||
// caller should pass to draw.register_texture alongside texture_buf.
|
// caller should pass to draw.register_texture alongside texture_buf.
|
||||||
//
|
//
|
||||||
|
// quiet_zone adds that many `light` pixels on each side; the spec value is 4.
|
||||||
|
// Final dimension is qrcode.get_size + 2*quiet_zone on each axis.
|
||||||
|
//
|
||||||
// Returns ok=false when:
|
// Returns ok=false when:
|
||||||
// - qrcode_buf is invalid (qrcode.get_size returns 0).
|
// - qrcode_buf is invalid (qrcode.get_size returns 0).
|
||||||
// - texture_buf is smaller than texture_size(qrcode_buf).
|
// - quiet_zone is negative.
|
||||||
|
// - texture_buf is smaller than texture_size(qrcode_buf, quiet_zone).
|
||||||
@(require_results)
|
@(require_results)
|
||||||
to_texture :: proc(
|
to_texture :: proc(
|
||||||
qrcode_buf: []u8,
|
qrcode_buf: []u8,
|
||||||
texture_buf: []u8,
|
texture_buf: []u8,
|
||||||
dark: draw.Color = DFT_QR_DARK,
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
|
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||||
) -> (
|
) -> (
|
||||||
desc: draw.Texture_Desc,
|
desc: draw.Texture_Desc,
|
||||||
ok: bool,
|
ok: bool,
|
||||||
) {
|
) {
|
||||||
size := qrcode.get_size(qrcode_buf)
|
size := qrcode.get_size(qrcode_buf)
|
||||||
if size == 0 do return {}, false
|
if size == 0 || quiet_zone < 0 do return
|
||||||
if len(texture_buf) < size * size * 4 do return {}, false
|
padded_size := size + 2 * quiet_zone
|
||||||
|
if len(texture_buf) < padded_size * padded_size * 4 do return
|
||||||
|
|
||||||
|
// Type-pun to []Color so each store is a single 32-bit write.
|
||||||
|
pixels := mem.slice_data_cast([]draw.Color, texture_buf[:padded_size * padded_size * 4])
|
||||||
|
|
||||||
|
// Bulk-fill with light: handles the border and every light QR module at once.
|
||||||
|
slice.fill(pixels, light)
|
||||||
|
|
||||||
|
// Overwrite only the dark modules, offset by the quiet-zone border.
|
||||||
for y in 0 ..< size {
|
for y in 0 ..< size {
|
||||||
|
row := (y + quiet_zone) * padded_size + quiet_zone
|
||||||
for x in 0 ..< size {
|
for x in 0 ..< size {
|
||||||
i := (y * size + x) * 4
|
if qrcode.get_module(qrcode_buf, x, y) {
|
||||||
c := dark if qrcode.get_module(qrcode_buf, x, y) else light
|
pixels[row + x] = dark
|
||||||
texture_buf[i + 0] = c[0]
|
}
|
||||||
texture_buf[i + 1] = c[1]
|
|
||||||
texture_buf[i + 2] = c[2]
|
|
||||||
texture_buf[i + 3] = c[3]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return draw.Texture_Desc {
|
return draw.Texture_Desc {
|
||||||
width = u32(size),
|
width = u32(padded_size),
|
||||||
height = u32(size),
|
height = u32(padded_size),
|
||||||
depth_or_layers = 1,
|
depth_or_layers = 1,
|
||||||
type = .D2,
|
type = .D2,
|
||||||
format = .R8G8B8A8_UNORM,
|
format = .R8G8B8A8_UNORM,
|
||||||
@@ -71,19 +88,20 @@ register_texture_from_raw :: proc(
|
|||||||
qrcode_buf: []u8,
|
qrcode_buf: []u8,
|
||||||
dark: draw.Color = DFT_QR_DARK,
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
|
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) -> (
|
) -> (
|
||||||
texture: draw.Texture_Id,
|
texture: draw.Texture_Id,
|
||||||
ok: bool,
|
ok: bool,
|
||||||
) {
|
) {
|
||||||
tex_size := texture_size(qrcode_buf)
|
tex_size := texture_size(qrcode_buf, quiet_zone)
|
||||||
if tex_size == 0 do return draw.INVALID_TEXTURE, false
|
if tex_size == 0 do return draw.INVALID_TEXTURE, false
|
||||||
|
|
||||||
pixels, alloc_err := make([]u8, tex_size, temp_allocator)
|
pixels, alloc_err := make([]u8, tex_size, temp_allocator)
|
||||||
if alloc_err != nil do return draw.INVALID_TEXTURE, false
|
if alloc_err != nil do return draw.INVALID_TEXTURE, false
|
||||||
defer delete(pixels, temp_allocator)
|
defer delete(pixels, temp_allocator)
|
||||||
|
|
||||||
desc := to_texture(qrcode_buf, pixels, dark, light) or_return
|
desc := to_texture(qrcode_buf, pixels, dark, light, quiet_zone) or_return
|
||||||
return draw.register_texture(desc, pixels)
|
return draw.register_texture(desc, pixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +121,7 @@ register_texture_from_text :: proc(
|
|||||||
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
||||||
dark: draw.Color = DFT_QR_DARK,
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
|
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) -> (
|
) -> (
|
||||||
texture: draw.Texture_Id,
|
texture: draw.Texture_Id,
|
||||||
@@ -123,7 +142,7 @@ register_texture_from_text :: proc(
|
|||||||
temp_allocator,
|
temp_allocator,
|
||||||
) or_return
|
) or_return
|
||||||
|
|
||||||
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
|
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture.
|
// Encodes arbitrary binary data as a QR Code and registers the result as an RGBA texture.
|
||||||
@@ -142,6 +161,7 @@ register_texture_from_binary :: proc(
|
|||||||
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
boost_ecl: bool = DFT_QR_BOOST_ECL,
|
||||||
dark: draw.Color = DFT_QR_DARK,
|
dark: draw.Color = DFT_QR_DARK,
|
||||||
light: draw.Color = DFT_QR_LIGHT,
|
light: draw.Color = DFT_QR_LIGHT,
|
||||||
|
quiet_zone: int = DFT_QR_QUIET_ZONE,
|
||||||
temp_allocator := context.temp_allocator,
|
temp_allocator := context.temp_allocator,
|
||||||
) -> (
|
) -> (
|
||||||
texture: draw.Texture_Id,
|
texture: draw.Texture_Id,
|
||||||
@@ -162,18 +182,10 @@ register_texture_from_binary :: proc(
|
|||||||
temp_allocator,
|
temp_allocator,
|
||||||
) or_return
|
) or_return
|
||||||
|
|
||||||
return register_texture_from_raw(qrcode_buf, dark, light, temp_allocator)
|
return register_texture_from_raw(qrcode_buf, dark, light, quiet_zone, temp_allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
register_texture_from :: proc {
|
register_texture_from :: proc {
|
||||||
register_texture_from_text,
|
register_texture_from_text,
|
||||||
register_texture_from_binary,
|
register_texture_from_binary,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default fit=.Fit preserves the QR's square aspect; override as needed.
|
|
||||||
clay_image :: #force_inline proc(
|
|
||||||
texture: draw.Texture_Id,
|
|
||||||
tint: draw.Color = draw.DFT_TINT,
|
|
||||||
) -> draw.Clay_Image_Data {
|
|
||||||
return draw.clay_image_data(texture, fit = .Fit, tint = tint)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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_TEXT :: "hellope-text"
|
||||||
EX_HELLOPE_CLAY :: "hellope-clay"
|
EX_HELLOPE_CLAY :: "hellope-clay"
|
||||||
EX_HELLOPE_CUSTOM :: "hellope-custom"
|
EX_HELLOPE_CUSTOM :: "hellope-custom"
|
||||||
|
EX_CLAY_BORDERS :: "clay-borders"
|
||||||
EX_TEXTURES :: "textures"
|
EX_TEXTURES :: "textures"
|
||||||
EX_GAUSSIAN_BLUR :: "gaussian-blur"
|
EX_GAUSSIAN_BLUR :: "gaussian-blur"
|
||||||
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
|
EX_GAUSSIAN_BLUR_DEBUG :: "gaussian-blur-debug"
|
||||||
@@ -23,6 +24,8 @@ AVAILABLE_EXAMPLES_MSG ::
|
|||||||
", " +
|
", " +
|
||||||
EX_HELLOPE_CUSTOM +
|
EX_HELLOPE_CUSTOM +
|
||||||
", " +
|
", " +
|
||||||
|
EX_CLAY_BORDERS +
|
||||||
|
", " +
|
||||||
EX_TEXTURES +
|
EX_TEXTURES +
|
||||||
", " +
|
", " +
|
||||||
EX_GAUSSIAN_BLUR +
|
EX_GAUSSIAN_BLUR +
|
||||||
@@ -81,6 +84,7 @@ main :: proc() {
|
|||||||
case EX_HELLOPE_CUSTOM: hellope_custom()
|
case EX_HELLOPE_CUSTOM: hellope_custom()
|
||||||
case EX_HELLOPE_SHAPES: hellope_shapes()
|
case EX_HELLOPE_SHAPES: hellope_shapes()
|
||||||
case EX_HELLOPE_TEXT: hellope_text()
|
case EX_HELLOPE_TEXT: hellope_text()
|
||||||
|
case EX_CLAY_BORDERS: clay_borders()
|
||||||
case EX_TEXTURES: textures()
|
case EX_TEXTURES: textures()
|
||||||
case EX_GAUSSIAN_BLUR: gaussian_blur()
|
case EX_GAUSSIAN_BLUR: gaussian_blur()
|
||||||
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
|
case EX_GAUSSIAN_BLUR_DEBUG: gaussian_blur_debug()
|
||||||
|
|||||||
@@ -166,12 +166,14 @@ textures :: proc() {
|
|||||||
|
|
||||||
ROW2_Y :: f32(190)
|
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.Color{255, 255, 255, 255}) // white bg
|
||||||
draw.rectangle(
|
draw.rectangle(
|
||||||
base_layer,
|
base_layer,
|
||||||
{COL1, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
{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},
|
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(
|
draw.text(
|
||||||
base_layer,
|
base_layer,
|
||||||
@@ -182,7 +184,7 @@ textures :: proc() {
|
|||||||
color = draw.WHITE,
|
color = draw.WHITE,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rounded corners
|
// Rounded corners + outline traces the rounded shape.
|
||||||
draw.rectangle(
|
draw.rectangle(
|
||||||
base_layer,
|
base_layer,
|
||||||
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
{COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE},
|
||||||
@@ -192,6 +194,8 @@ textures :: proc() {
|
|||||||
uv_rect = {0, 0, 1, 1},
|
uv_rect = {0, 0, 1, 1},
|
||||||
sampler = .Nearest_Clamp,
|
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),
|
radii = draw.uniform_radii({COL2, ROW2_Y, ITEM_SIZE, ITEM_SIZE}, 0.3),
|
||||||
)
|
)
|
||||||
draw.text(
|
draw.text(
|
||||||
@@ -203,7 +207,7 @@ textures :: proc() {
|
|||||||
color = draw.WHITE,
|
color = draw.WHITE,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rotating
|
// Rotating + outline rotates with the texture.
|
||||||
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
|
rot_rect := draw.Rectangle{COL3, ROW2_Y, ITEM_SIZE, ITEM_SIZE}
|
||||||
draw.rectangle(
|
draw.rectangle(
|
||||||
base_layer,
|
base_layer,
|
||||||
@@ -214,6 +218,8 @@ textures :: proc() {
|
|||||||
uv_rect = {0, 0, 1, 1},
|
uv_rect = {0, 0, 1, 1},
|
||||||
sampler = .Nearest_Clamp,
|
sampler = .Nearest_Clamp,
|
||||||
},
|
},
|
||||||
|
outline_color = draw.WHITE,
|
||||||
|
outline_width = 2,
|
||||||
origin = draw.center_of(rot_rect),
|
origin = draw.center_of(rot_rect),
|
||||||
rotation = spin_angle,
|
rotation = spin_angle,
|
||||||
)
|
)
|
||||||
@@ -282,7 +288,7 @@ textures :: proc() {
|
|||||||
color = draw.WHITE,
|
color = draw.WHITE,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Per-corner radii
|
// Per-corner radii + outline traces the asymmetric corner shape.
|
||||||
draw.rectangle(
|
draw.rectangle(
|
||||||
base_layer,
|
base_layer,
|
||||||
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
|
{COL4, ROW3_Y, FIT_SIZE, FIT_SIZE},
|
||||||
@@ -292,6 +298,8 @@ textures :: proc() {
|
|||||||
uv_rect = {0, 0, 1, 1},
|
uv_rect = {0, 0, 1, 1},
|
||||||
sampler = .Nearest_Clamp,
|
sampler = .Nearest_Clamp,
|
||||||
},
|
},
|
||||||
|
outline_color = draw.Color{255, 100, 100, 255},
|
||||||
|
outline_width = 3,
|
||||||
radii = {20, 0, 20, 0},
|
radii = {20, 0, 20, 0},
|
||||||
)
|
)
|
||||||
draw.text(
|
draw.text(
|
||||||
@@ -321,12 +329,14 @@ textures :: proc() {
|
|||||||
sampler = .Nearest_Clamp,
|
sampler = .Nearest_Clamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Textured circle
|
// Textured circle + outline (textured shape with built-in border).
|
||||||
draw.circle(
|
draw.circle(
|
||||||
base_layer,
|
base_layer,
|
||||||
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
{SHAPE_COL1 + SHAPE_SIZE / 2, ROW4_Y + SHAPE_SIZE / 2},
|
||||||
SHAPE_SIZE / 2,
|
SHAPE_SIZE / 2,
|
||||||
checker_fill,
|
checker_fill,
|
||||||
|
outline_color = draw.WHITE,
|
||||||
|
outline_width = 2,
|
||||||
)
|
)
|
||||||
draw.text(
|
draw.text(
|
||||||
base_layer,
|
base_layer,
|
||||||
|
|||||||
@@ -56,16 +56,6 @@ Texture_Slot :: struct {
|
|||||||
// GLOB.pending_texture_releases : [dynamic]Texture_Id
|
// GLOB.pending_texture_releases : [dynamic]Texture_Id
|
||||||
// GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler
|
// GLOB.samplers : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler
|
||||||
|
|
||||||
Clay_Image_Data :: struct {
|
|
||||||
texture_id: Texture_Id,
|
|
||||||
fit: Fit_Mode,
|
|
||||||
tint: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
|
|
||||||
return {texture_id = id, fit = fit, tint = tint}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
// ----- Registration -------------
|
// ----- Registration -------------
|
||||||
// ---------------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user