Skip to content

Instantly share code, notes, and snippets.

@rsms
Last active July 10, 2026 00:01
Show Gist options
  • Select an option

  • Save rsms/d7c619a47e8382cba8cdb32ebc913c56 to your computer and use it in GitHub Desktop.

Select an option

Save rsms/d7c619a47e8382cba8cdb32ebc913c56 to your computer and use it in GitHub Desktop.
// Mesh gradient editor — interactive MESH_GRADIENT showcase.
//
// A sidebar lists up to 16 color points: click a row to select it, click the
// "+" row to add a point, and DELETE/BACKSPACE removes the selected point
// (the last one always stays). The picker at the bottom-left edits the
// selected color: an HSV square + hue strip working in Display P3, plus
// per-channel HDR gain sliders (100%..400% of SDR) for EDR displays. Points
// are drawn as ring handles over the gradient; drag them to move the colors
// around. Handles fade out when the mouse has been still for two seconds,
// and the layout follows the window size.
//
// The gradient itself is one MESH_GRADIENT command re-submitted every frame:
// scattered {x, y, color} points blended in OKLab. Point positions are kept
// normalized so the mesh stretches with the window.
#include <playbit/playbit.h>
////////////////////////////////////////////////////////////////////////////////////////////////////
// layout (dp; the picker anchors to the sidebar's bottom-left corner)
enum {
kInitialWindowW = 880,
kInitialWindowH = 720,
kSidebarW = 236, // fits three 64dp gain sliders with 10dp gaps
kRowH = 34,
kSwatchSize = 16,
kSvX = 12,
kSvW = kSidebarW - 2 * kSvX,
kSvH = 176,
kHueH = 16,
kSliderH = 16,
kSliderW = 64, // 16dp per 100%; the whole bar is 400%
kSliderPer100 = 16, // bar dp per 100% intensity
kHandleRadius = 9,
kMaxPoints = 16,
};
static f32 gWinW = kInitialWindowW; // window content size (dp)
static f32 gWinH = kInitialWindowH;
#define SLIDER_ROW_Y (gWinH - 48) // HDR gain sliders + sun icon
#define HUE_Y (gWinH - 72)
#define SV_Y (gWinH - 72 - 8 - kSvH)
#define CANVAS_W (gWinW - kSidebarW)
static f32 SliderX(u32 channel) {
return (f32)kSvX + (f32)channel * (kSliderW + 10.0f);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// lil draw API (see examples/draw-v2.c)
typedef struct PBCanvasDrawCtx {
PBSysHandle canvas;
u64 cmdsStorage[2048];
PBU64Array cmds; // holds variably-sized PBSysCanvasCmd structs
PBMem ma;
} PBCanvasDrawCtx;
static void BeginDraw(PBCanvasDrawCtx* g, PBSysHandle canvas, PBMem ma) {
g->canvas = canvas;
PBAnyArray_INIT_WITH_STORAGE(g, cmds, cmdsStorage);
g->ma = ma;
}
static void CommitDraw(PBCanvasDrawCtx* g, u64 clearColor) {
const PBSysCanvasCmd* cmds = (const PBSysCanvasCmd*)g->cmds.v;
u32 cmdsSize = g->cmds.len * sizeof(u64);
PBSysErr err = PBSysCanvasRender(g->canvas, clearColor, cmds, cmdsSize, 0);
if UNLIKELY (err && err != PBSysErr_AGAIN)
PanicOnErr(err);
PBArrayFree(&g->cmds, g->ma);
}
static void* AppendRaw(PBCanvasDrawCtx* g, u16 size, u16 kind) {
Expect((size & 7) == 0);
u64* p = PBArrayAlloc(&g->cmds, g->ma, size / sizeof(u64));
ExpectNotNull(p);
memset(p, 0, size);
PBSysCanvasCmd* cmd = (PBSysCanvasCmd*)p;
cmd->size = size;
cmd->kind = kind;
return cmd;
}
static f32 gDp = 1.0f; // pixels per dp
#define PX(v) ((f32)(v) * gDp)
static PBSysCanvasAABB Box(f32 x, f32 y, f32 w, f32 h) {
return (
PBSysCanvasAABB){ PBRound(PX(x)), PBRound(PX(y)), PBRound(PX(x + w)), PBRound(PX(y + h)) };
}
static void FillRect(PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 color) {
PBSysCanvasCmdRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_RECT);
cmd->bounds = Box(x, y, w, h);
cmd->fillColor = color;
}
static void FillRoundedRect(PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 color, f32 radius) {
PBSysCanvasCmdRoundedRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_ROUNDED_RECT);
cmd->bounds = Box(x, y, w, h);
cmd->fillColor = color;
u16 r = (u16)PBRound(PX(radius));
for (u32 i = 0; i < 4; i++)
cmd->cornerRadius[i] = r;
}
static void StrokeRect(
PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 color, f32 width, f32 radius) {
PBSysCanvasCmdStrokeRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_STROKE_RECT);
cmd->bounds = Box(x, y, w, h);
u16 r = (u16)PBRound(PX(radius));
for (u32 i = 0; i < 4; i++)
cmd->cornerRadius[i] = r;
cmd->strokeColor = color;
cmd->strokeWidth = PX(width);
}
static void FillDisc(PBCanvasDrawCtx* g, f32 cx, f32 cy, f32 radius, u64 color) {
FillRoundedRect(g, cx - radius, cy - radius, radius * 2, radius * 2, color, radius);
}
// circle outline (a stroked rounded rect whose radius makes it a circle)
static void StrokeDisc(PBCanvasDrawCtx* g, f32 cx, f32 cy, f32 radius, f32 width, u64 color) {
StrokeRect(g, cx - radius, cy - radius, radius * 2, radius * 2, color, width, radius);
}
static void FillGradientRect4(
PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, u64 tl, u64 tr, u64 bl, u64 br) {
PBSysCanvasCmdGradientRect* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_GRADIENT_RECT);
cmd->bounds = Box(x, y, w, h);
cmd->fillColors[0] = tl;
cmd->fillColors[1] = tr;
cmd->fillColors[2] = bl;
cmd->fillColors[3] = br;
}
static void FillMeshGradient(
PBCanvasDrawCtx* g,
f32 x,
f32 y,
f32 w,
f32 h,
f32 falloff,
const PBSysCanvasMeshPoint* points,
u16 pointCount) {
Expect(pointCount <= PBSysCanvasMeshGradient_MAX_POINTS);
u16 size = (u16)(sizeof(PBSysCanvasCmdMeshGradient) + pointCount * sizeof(*points));
PBSysCanvasCmdMeshGradient* cmd = AppendRaw(g, size, PBSysCanvasCmdKind_MESH_GRADIENT);
cmd->bounds = Box(x, y, w, h);
cmd->space = PBSysCanvasGradientSpace_OKLAB;
cmd->pointCount = pointCount;
cmd->falloff = falloff;
memcpy(cmd->points, points, pointCount * sizeof(*points));
}
static void DrawTextPlan(PBCanvasDrawCtx* g, PBSysHandle textPlan, f32 x, f32 y, u64 color) {
if (textPlan == PBSysHandle_INVALID)
return;
PBSysCanvasCmdText* cmd = AppendRaw(g, sizeof(*cmd), PBSysCanvasCmdKind_TEXT);
cmd->bounds = Box(x, y, 0, 0);
cmd->textPlan = textPlan;
cmd->flags = PBSysCanvasTextFlag_OVERRIDE_COLOR;
cmd->fillColor = color;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// color: HSV picker semantics on encoded Display P3 values (no sRGB gamut
// mapping). Wire colors are the renderer's native space, linear extended P3:
// encoded components decode through the P3 transfer curve (the sRGB curve
// over P3 primaries) and per-channel HDR gains scale the linear values past
// the SDR ceiling.
typedef struct Hsv {
f32 h, s, v; // all [0, 1]
} Hsv;
static void HsvToEncodedP3(Hsv c, f32* r, f32* g, f32* b) {
f32 h = c.h >= 1.0f ? 0.0f : c.h * 6.0f;
i32 sector = (i32)h;
f32 f = h - (f32)sector;
f32 p = c.v * (1.0f - c.s);
f32 q = c.v * (1.0f - c.s * f);
f32 t = c.v * (1.0f - c.s * (1.0f - f));
switch (sector) {
case 0: *r = c.v, *g = t, *b = p; break;
case 1: *r = q, *g = c.v, *b = p; break;
case 2: *r = p, *g = c.v, *b = t; break;
case 3: *r = p, *g = q, *b = c.v; break;
case 4: *r = t, *g = p, *b = c.v; break;
default: *r = c.v, *g = p, *b = q; break;
}
}
// Display P3 transfer decode
static f32 DecodeP3(f32 v) {
return v <= 0.04045f ? v / 12.92f : PBPowF32((v + 0.055f) / 1.055f, 2.4f);
}
static u64 HsvWireColorHDR(Hsv c, const f32 gain[3]) {
f32 r, g, b;
HsvToEncodedP3(c, &r, &g, &b);
return PBColorRgbHDR(DecodeP3(r) * gain[0], DecodeP3(g) * gain[1], DecodeP3(b) * gain[2]).u64;
}
static u64 HsvWireColor(Hsv c) { // SDR (UI chrome: swatches, knobs, picker)
f32 r, g, b;
HsvToEncodedP3(c, &r, &g, &b);
return PBColorRgb(DecodeP3(r), DecodeP3(g), DecodeP3(b)).u64;
}
static void FillHueStrip(PBCanvasDrawCtx* g, f32 x, f32 y, f32 w, f32 h, f32 radius) {
PBSysCanvasGradientStop stops[7];
for (u32 i = 0; i < 7; i++) {
stops[i].offset = (f32)i / 6.0f;
stops[i]._reserved = 0;
stops[i].color = HsvWireColor((Hsv){ (f32)i / 6.0f, 1.0f, 1.0f });
}
u16 size = (u16)(sizeof(PBSysCanvasCmdGradient) + 7 * sizeof(stops[0]));
PBSysCanvasCmdGradient* cmd = AppendRaw(g, size, PBSysCanvasCmdKind_GRADIENT);
cmd->bounds = Box(x, y, w, h);
u16 r = (u16)PBRound(PX(radius));
for (u32 i = 0; i < 4; i++)
cmd->cornerRadius[i] = r;
cmd->kind = PBSysCanvasGradientKind_LINEAR;
cmd->space = PBSysCanvasGradientSpace_LINEAR_P3;
cmd->spread = PBSysCanvasGradientSpread_PAD;
cmd->stopCount = 7;
cmd->p1[0] = PX(w);
memcpy(cmd->stops, stops, sizeof(stops));
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// app state
typedef struct Point {
f32 x, y; // normalized [0, 1] within the canvas area
Hsv hsv; // encoded Display P3 HSV
f32 gain[3]; // per-channel linear HDR gain, [1, 4] (1 = SDR)
} Point;
typedef enum DragMode { DRAG_NONE, DRAG_HANDLE, DRAG_SV, DRAG_HUE, DRAG_SLIDER } DragMode;
// cached value labels for one sidebar row (rebuilt when the color changes)
typedef struct RowLabels {
PBSysHandle plan[3];
f32 vals[3];
bool valid;
} RowLabels;
// cached "%d%%" labels for the gain sliders (showing the selected point)
typedef struct GainLabels {
PBSysHandle plan[3];
i32 pct[3];
bool valid;
} GainLabels;
static bool gHdrSupported = false; // PBSysCanvasFeature_HDR on the surface canvas
static struct {
Point points[kMaxPoints];
u32 count;
u32 sel;
DragMode drag;
u32 dragChannel; // for DRAG_SLIDER
f32 dragStartX; // pointer x at DRAG_SLIDER start (dp)
f32 dragStartGain; // gain at DRAG_SLIDER start
PBTime lastPointerMove;
RowLabels rowLabels[kMaxPoints];
GainLabels gainLabels;
} gApp;
static PBSysHandle MakeLabelStr(const char* text, f32 sizeDp, f32 weight) {
usize len = strlen(text);
PBSysPositionedTextAttribute attributes[] = {
{ .start = 0, .attribute = { .type = PBSysTextAttribute_SIZE, .f = sizeDp * gDp } },
{ .start = 0, .attribute = { .type = PBSysTextAttribute_WEIGHT, .f = weight } },
// tabular numerals so value columns don't shift as digits change
{ .start = 0,
.attribute = { .type = PBSysTextAttribute_OTYPE_FEATURE,
.feature = { .tag = ('t' << 24) | ('n' << 16) | ('u' << 8) | 'm',
.value = 1 } } },
};
PBSysHandle textPlan = PBSysWindowTextplanCreate2(
PBWindowMain().handle,
(const u8*)text,
len,
attributes,
sizeof(attributes[0]),
PBCountOf(attributes));
Expect(textPlan > 0);
PanicOnErr(PBSysTextplanLayout(textPlan, PBTextPlanLayout_ONE_LINE, 0, 0));
return textPlan;
}
// returns the row's cached r/g/b value labels, rebuilding them if the color
// changed since last frame
static const RowLabels* RowLabelsFor(u32 index) {
RowLabels* labels = &gApp.rowLabels[index];
f32 rgb[3];
HsvToEncodedP3(gApp.points[index].hsv, &rgb[0], &rgb[1], &rgb[2]);
if (labels->valid && labels->vals[0] == rgb[0] && labels->vals[1] == rgb[1]
&& labels->vals[2] == rgb[2])
{
return labels;
}
for (u32 i = 0; i < 3; i++) {
if (labels->valid)
(void)PBSysHandleClose(labels->plan[i]);
// "0.478", "0.62", "1.0": three decimals, trailing zeros trimmed,
// but always keeping one decimal
char buf[16];
u32 n = PBSnprintf(buf, sizeof(buf), "%.3f", (f64)rgb[i]);
while (n > 3 && buf[n - 1] == '0')
buf[--n] = 0;
labels->plan[i] = MakeLabelStr(buf, 12, 500);
labels->vals[i] = rgb[i];
}
labels->valid = true;
return labels;
}
static void InvalidateRowLabels(void) {
for (u32 i = 0; i < kMaxPoints; i++) {
if (gApp.rowLabels[i].valid) {
for (u32 c = 0; c < 3; c++)
(void)PBSysHandleClose(gApp.rowLabels[i].plan[c]);
gApp.rowLabels[i].valid = false;
}
}
}
static const GainLabels* GainLabelsForSelected(void) {
GainLabels* labels = &gApp.gainLabels;
i32 pct[3];
for (u32 i = 0; i < 3; i++)
pct[i] = (i32)PBRound(gApp.points[gApp.sel].gain[i] * 100.0f);
if (labels->valid && labels->pct[0] == pct[0] && labels->pct[1] == pct[1]
&& labels->pct[2] == pct[2])
{
return labels;
}
for (u32 i = 0; i < 3; i++) {
if (labels->valid)
(void)PBSysHandleClose(labels->plan[i]);
char buf[8];
PBSnprintf(buf, sizeof(buf), "%d%%", pct[i]);
labels->plan[i] = MakeLabelStr(buf, 11, 500);
labels->pct[i] = pct[i];
}
labels->valid = true;
return labels;
}
static void AddPoint(f32 x, f32 y, Hsv hsv) {
if (gApp.count >= kMaxPoints)
return;
gApp.points[gApp.count] = (Point){ .x = x, .y = y, .hsv = hsv, .gain = { 1, 1, 1 } };
gApp.sel = gApp.count;
gApp.count++;
}
// removes the selected point; the last remaining point cannot be removed
static void RemoveSelectedPoint(void) {
if (gApp.count <= 1)
return;
memmove(
&gApp.points[gApp.sel],
&gApp.points[gApp.sel + 1],
(gApp.count - gApp.sel - 1) * sizeof(Point));
gApp.count--;
if (gApp.sel >= gApp.count)
gApp.sel = gApp.count - 1;
// indices shifted: drop all cached labels and let them rebuild lazily
InvalidateRowLabels();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// drawing
static const u64 kInk = PB_COLOR_RGB(0.015f, 0.017f, 0.022f).u64;
static const u64 kSeparator = PB_COLOR_RGB(0.78f, 0.78f, 0.79f).u64;
static const u64 kSelectedBg = PB_COLOR_RGB(0.018f, 0.15f, 0.86f).u64;
static const u64 kGray = PB_COLOR_RGB(0.26f, 0.27f, 0.29f).u64;
// pastel channel tints for the gain slider fills (linear P3)
static const u64 kTintR = PB_COLOR_RGB(0.93f, 0.30f, 0.17f).u64;
static const u64 kTintG = PB_COLOR_RGB(0.27f, 0.70f, 0.25f).u64;
static const u64 kTintB = PB_COLOR_RGB(0.20f, 0.38f, 0.87f).u64;
static const u64 kSliderBorder = PB_COLOR_RGBA(0, 0, 0, 0.38f).u64;
static void DrawSunIcon(PBCanvasDrawCtx* g, f32 cx, f32 cy) {
StrokeDisc(g, cx, cy, 3.5f, 1.5f, kGray);
FillRect(g, cx - 7, cy - 0.5f, 2.5f, 1, kGray);
FillRect(g, cx + 4.5f, cy - 0.5f, 2.5f, 1, kGray);
FillRect(g, cx - 0.5f, cy - 7, 1, 2.5f, kGray);
FillRect(g, cx - 0.5f, cy + 4.5f, 1, 2.5f, kGray);
}
static void DrawGainSliders(PBCanvasDrawCtx* g) {
const u64 tints[3] = { kTintR, kTintG, kTintB };
const Point* p = &gApp.points[gApp.sel];
const GainLabels* labels = GainLabelsForSelected();
DrawSunIcon(g, 20, SLIDER_ROW_Y + kSliderH + 13);
for (u32 i = 0; i < 3; i++) {
f32 x = SliderX(i);
f32 y = SLIDER_ROW_Y;
FillRect(g, x, y, kSliderW, kSliderH, PBColor_White.u64);
// the bar directly represents intensity: 100% = 16dp, 400% = full
FillRect(g, x, y, p->gain[i] * kSliderPer100, kSliderH, tints[i]);
// ticks every 16dp (at each 100% step)
for (u32 tk = 1; tk < 4; tk++)
FillRect(g, x + (f32)(kSliderPer100 * tk), y + kSliderH - 4, 1, 4, kSliderBorder);
// border last, on top of the bar
StrokeRect(g, x, y, kSliderW, kSliderH, kSliderBorder, 1, 0);
// right-aligned percent label (tabular digits: all values same width)
DrawTextPlan(g, labels->plan[i], x + kSliderW - 32, y + kSliderH + 6, kInk);
}
}
static void DrawSidebar(PBCanvasDrawCtx* g) {
FillRect(g, 0, 0, kSidebarW, gWinH, PBColor_White.u64);
// point rows
for (u32 i = 0; i < gApp.count; i++) {
f32 y = (f32)(i * kRowH);
bool selected = i == gApp.sel;
if (selected)
FillRect(g, 0, y, kSidebarW, kRowH, kSelectedBg);
FillRoundedRect(
g, 12, y + 9, kSwatchSize, kSwatchSize, HsvWireColor(gApp.points[i].hsv), 4);
const RowLabels* labels = RowLabelsFor(i);
u64 textColor = selected ? PBColor_White.u64 : kInk;
DrawTextPlan(g, labels->plan[0], 40, y + 9, textColor);
DrawTextPlan(g, labels->plan[1], 87, y + 9, textColor);
DrawTextPlan(g, labels->plan[2], 134, y + 9, textColor);
if (!selected)
FillRect(g, 0, y + kRowH - 1, kSidebarW, 1, kSeparator);
}
// "+" row (hidden once the mesh is full)
if (gApp.count < kMaxPoints) {
f32 y = (f32)(gApp.count * kRowH);
f32 cx = kSidebarW / 2.0f;
f32 cy = y + kRowH / 2.0f;
FillRect(g, cx - 7, cy - 1, 14, 2, kGray);
FillRect(g, cx - 1, cy - 7, 2, 14, kGray);
FillRect(g, 0, y + kRowH - 1, kSidebarW, 1, kSeparator);
}
// HSV picker for the selected point
Hsv sel = gApp.points[gApp.sel].hsv;
// saturation/value square: white -> hue across, black downward
u64 hue = HsvWireColor((Hsv){ sel.h, 1.0f, 1.0f });
FillGradientRect4(
g, kSvX, SV_Y, kSvW, kSvH, PBColor_White.u64, hue, PBColor_Black.u64, PBColor_Black.u64);
// knob
f32 kx = kSvX + sel.s * kSvW;
f32 ky = SV_Y + (1.0f - sel.v) * kSvH;
StrokeDisc(g, kx, ky, 5, 2, PBColor_White.u64);
FillDisc(g, kx, ky, 3.5f, HsvWireColor(sel));
// hue strip + knob
FillHueStrip(g, kSvX, HUE_Y, kSvW, kHueH, kHueH / 2.0f);
f32 hx = kSvX + kHueH / 2.0f + sel.h * (kSvW - kHueH);
StrokeDisc(g, hx, HUE_Y + kHueH / 2.0f, 5, 2, PBColor_White.u64);
FillDisc(g, hx, HUE_Y + kHueH / 2.0f, 3.5f, HsvWireColor((Hsv){ sel.h, 1, 1 }));
if (gHdrSupported)
DrawGainSliders(g);
}
static void Draw(PBSysHandle canvas) {
// follow the window size (the picker anchors bottom-left, the mesh
// stretches over the whole canvas area)
PBSize size = PBWindowContentSize(PBWindowMain());
if (size.width > kSidebarW && size.height > 0) {
gWinW = size.width;
gWinH = size.height;
}
PBCanvasDrawCtx g;
BeginDraw(&g, canvas, kMemGPA);
// the mesh gradient fills the canvas area (point coords are normalized)
PBSysCanvasMeshPoint meshPoints[kMaxPoints];
for (u32 i = 0; i < gApp.count; i++) {
meshPoints[i] = (PBSysCanvasMeshPoint){
.x = PX(gApp.points[i].x * CANVAS_W),
.y = PX(gApp.points[i].y * gWinH),
.weight = 1.0f,
.color = HsvWireColorHDR(gApp.points[i].hsv, gApp.points[i].gain),
};
}
FillMeshGradient(&g, kSidebarW, 0, CANVAS_W, gWinH, 2.2f, meshPoints, (u16)gApp.count);
// point handles fade out when the mouse has been still for 2 seconds
if (PBTimeNow() - gApp.lastPointerMove < 2 * PB_TIME_SECOND) {
for (u32 i = 0; i < gApp.count; i++) {
f32 cx = kSidebarW + gApp.points[i].x * CANVAS_W;
f32 cy = gApp.points[i].y * gWinH;
StrokeDisc(&g, cx, cy, kHandleRadius, 3, PBColor_White.u64);
}
}
DrawSidebar(&g);
CommitDraw(&g, PBColor_White.u64);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// interaction (pointer coordinates arrive in window dp)
static void ApplyDrag(f32 x, f32 y) {
Point* p = &gApp.points[gApp.sel];
switch (gApp.drag) {
case DRAG_HANDLE:
p->x = PBClamp((x - kSidebarW) / CANVAS_W, 0.0f, 1.0f);
p->y = PBClamp(y / gWinH, 0.0f, 1.0f);
break;
case DRAG_SV:
p->hsv.s = PBClamp((x - kSvX) / kSvW, 0.0f, 1.0f);
p->hsv.v = 1.0f - PBClamp((y - SV_Y) / kSvH, 0.0f, 1.0f);
break;
case DRAG_HUE:
p->hsv.h = PBClamp((x - (kSvX + kHueH / 2.0f)) / (kSvW - kHueH), 0.0f, 1.0f);
break;
case DRAG_SLIDER:
// relative: the bar moves by the delta dragged (16dp per 100%)
p->gain[gApp.dragChannel] = PBClamp(
gApp.dragStartGain + (x - gApp.dragStartX) / (f32)kSliderPer100, 1.0f, 4.0f);
break;
case DRAG_NONE: break;
}
}
static void OnPointerDown(f32 x, f32 y) {
// canvas: grab the nearest handle within reach
if (x >= kSidebarW) {
f32 bestDist = 16.0f; // grab radius (dp)
i32 best = -1;
for (u32 i = 0; i < gApp.count; i++) {
f32 dx = x - (kSidebarW + gApp.points[i].x * CANVAS_W);
f32 dy = y - gApp.points[i].y * gWinH;
f32 d = PBSqrtF32(dx * dx + dy * dy);
if (d < bestDist) {
bestDist = d;
best = (i32)i;
}
}
if (best >= 0) {
gApp.sel = (u32)best;
gApp.drag = DRAG_HANDLE;
ApplyDrag(x, y);
}
return;
}
// picker
if (x >= kSvX && x < kSvX + kSvW && y >= SV_Y && y < SV_Y + kSvH) {
gApp.drag = DRAG_SV;
ApplyDrag(x, y);
return;
}
if (x >= kSvX && x < kSvX + kSvW && y >= HUE_Y - 4 && y < HUE_Y + kHueH + 4) {
gApp.drag = DRAG_HUE;
ApplyDrag(x, y);
return;
}
if (gHdrSupported && y >= SLIDER_ROW_Y - 2 && y < SLIDER_ROW_Y + kSliderH + 2) {
for (u32 i = 0; i < 3; i++) {
if (x >= SliderX(i) - 2 && x < SliderX(i) + kSliderW + 2) {
gApp.drag = DRAG_SLIDER;
gApp.dragChannel = i;
gApp.dragStartX = x;
gApp.dragStartGain = gApp.points[gApp.sel].gain[i];
return;
}
}
}
// rows
u32 row = (u32)(y / kRowH);
if (row < gApp.count) {
gApp.sel = row;
} else if (row == gApp.count && gApp.count < kMaxPoints) {
// "+": add a point in the middle of the canvas with a fresh hue
Hsv hsv = { .h = (f32)(gApp.count * 5 % 16) / 16.0f, .s = 0.75f, .v = 0.95f };
AddPoint(0.5f, 0.5f, hsv);
}
}
void main(void) {
PBSysHandle window = PBWindowMain().handle;
Expect(window != PBSysHandle_INVALID);
PBObserve(window, U32_MAX);
PanicOnErr(PBSysWindowFrameSyncEnable(window, 1));
PBWindowSetContentSizeCentered(PBWindowMain(), kInitialWindowW, kInitialWindowH);
gDp = PBWindowScaleFactor(PBWindowMain());
PBSysHandle canvas = PBSysWindowOpenSurfaceCanvas(window, 0);
Expect(canvas != PBSysHandle_INVALID);
// the HDR gain sliders only appear when the display can actually show
// extended-range colors
gHdrSupported = (PBSysObjectFeatures(canvas) & (1llu << PBSysCanvasFeature_HDR)) != 0;
// the four starter points from the design mock
AddPoint(0.85f, 0.06f, (Hsv){ 0.594f, 0.522f, 1.0f }); // periwinkle
AddPoint(0.29f, 0.22f, (Hsv){ 0.424f, 0.816f, 0.255f }); // dark green
AddPoint(0.87f, 0.56f, (Hsv){ 0.097f, 1.0f, 1.0f }); // orange
AddPoint(0.28f, 0.96f, (Hsv){ 0.900f, 0.148f, 0.98f }); // pink
gApp.sel = 2;
gApp.lastPointerMove = PBTimeNow();
Draw(canvas); // render the initial frame explicitly (reveals the window)
for (PBEventAny ev; PBEventPoll(&ev.event, sizeof(ev), 1);) {
switch (ev.type) {
case PBEventType_POINTER_DOWN:
gApp.lastPointerMove = PBTimeNow();
OnPointerDown(ev.pointer.x, ev.pointer.y);
break;
case PBEventType_POINTER_MOVE:
gApp.lastPointerMove = PBTimeNow();
if (gApp.drag != DRAG_NONE)
ApplyDrag(ev.pointer.x, ev.pointer.y);
break;
case PBEventType_POINTER_UP:
case PBEventType_POINTER_CANCEL: gApp.drag = DRAG_NONE; break;
case PBEventType_KEY_DOWN:
if (ev.keyboard.keyCode == PBKeyboardKey_Delete
|| ev.keyboard.keyCode == PBKeyboardKey_Backspace)
{
RemoveSelectedPoint();
}
break;
case PBEventType_SIGNAL:
if (ev.signal.handle == window
&& (ev.signal.signals
& (PBSysWindowSignal_FRAME_SYNC | PBSysWindowSignal_RESIZE
| PBSysWindowSignal_REPAINT)))
{
Draw(canvas);
}
break;
default: break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment