DVX_GUI/widgets/slider/widgetSlider.c

418 lines
13 KiB
C

#define DVX_WIDGET_IMPL
// widgetSlider.c -- Slider (trackbar) widget
//
// A continuous-value selector with a draggable thumb on a groove track.
// Supports both horizontal and vertical orientation. The value maps
// linearly to thumb position using integer arithmetic only.
//
// Rendering: thin sunken groove for the track, raised 2px-bevel thumb,
// and a center tick line on the thumb for grip feedback. The groove
// uses reversed highlight/shadow (same as progress bar trough) for
// the recessed look. The thumb uses the standard raised bevel.
//
// Interaction model: clicking on the track jumps the thumb to that
// position (instant seek), while clicking on the thumb starts a drag
// via the global sDragSlider/sDragOffset state. The drag offset stores
// where within the thumb the user grabbed, so the thumb doesn't snap
// to the cursor center during drag -- this feels much more natural.
//
// Keyboard: arrow keys increment/decrement by a computed step that
// scales with range (step = range/100 for ranges > 100, else 1).
// Home/End jump to min/max. This gives ~100 keyboard steps regardless
// of the actual value range.
#include "dvxWidgetPlugin.h"
#define SLIDER_TRACK_H 4
#define SLIDER_THUMB_W 11
static int32_t sTypeId = -1;
typedef struct {
int32_t value;
int32_t minValue;
int32_t maxValue;
bool vertical;
int32_t dragOffset;
} SliderDataT;
// ============================================================
// widgetSliderCalcMinSize
// ============================================================
// Min size: 5 thumb-widths along the main axis gives enough room for
// the thumb to travel meaningfully. Cross-axis is thumb width + 4px
// margin so the groove has visual breathing room around the thumb.
void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
(void)font;
SliderDataT *d = (SliderDataT *)w->data;
if (d->vertical) {
w->calcMinW = SLIDER_THUMB_W + 4;
w->calcMinH = SLIDER_THUMB_W * 5;
} else {
w->calcMinW = SLIDER_THUMB_W * 5;
w->calcMinH = SLIDER_THUMB_W + 4;
}
}
// ============================================================
// widgetSliderOnKey
// ============================================================
void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
SliderDataT *d = (SliderDataT *)w->data;
int32_t range = d->maxValue - d->minValue;
int32_t pageStep = range / 10;
if (pageStep < 1) {
pageStep = 1;
}
// Arrow keys: step by 1. Page Up/Down: step by 10% of range.
// Home/End: jump to min/max.
if (d->vertical) {
if (key == (0x48 | 0x100)) {
d->value -= 1;
} else if (key == (0x50 | 0x100)) {
d->value += 1;
} else if (key == (0x49 | 0x100)) {
d->value -= pageStep;
} else if (key == (0x51 | 0x100)) {
d->value += pageStep;
} else if (key == (0x47 | 0x100)) {
d->value = d->minValue;
} else if (key == (0x4F | 0x100)) {
d->value = d->maxValue;
} else {
return;
}
} else {
if (key == (0x4B | 0x100) || key == (0x48 | 0x100)) {
d->value -= 1;
} else if (key == (0x4D | 0x100) || key == (0x50 | 0x100)) {
d->value += 1;
} else if (key == (0x49 | 0x100)) {
d->value -= pageStep;
} else if (key == (0x51 | 0x100)) {
d->value += pageStep;
} else if (key == (0x47 | 0x100)) {
d->value = d->minValue;
} else if (key == (0x4F | 0x100)) {
d->value = d->maxValue;
} else {
return;
}
}
d->value = clampInt(d->value, d->minValue, d->maxValue);
if (w->onChange) {
w->onChange(w);
}
wgtInvalidatePaint(w);
}
// ============================================================
// widgetSliderOnMouse
// ============================================================
// Mouse click distinguishes between thumb hit (start drag) and track
// hit (jump to position). The thumb hit area is the full SLIDER_THUMB_W
// pixel range at the current thumb position. The jump-to-position
// calculation centers the thumb at the click point by subtracting
// half the thumb width before converting pixel position to value.
//
// Drag state is stored in globals (sDragSlider, sDragOffset) rather
// than per-widget because only one slider can be dragged at a time.
// The event loop checks sDragSlider on mouse-move to continue the drag.
void widgetSliderOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
(void)root;
sFocusedWidget = hit;
SliderDataT *d = (SliderDataT *)hit->data;
int32_t range = d->maxValue - d->minValue;
if (range <= 0) {
return;
}
int32_t thumbRange;
int32_t thumbPos;
int32_t mousePos;
if (d->vertical) {
thumbRange = hit->h - SLIDER_THUMB_W;
thumbPos = ((d->value - d->minValue) * thumbRange) / range;
mousePos = vy - hit->y;
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
// Click on thumb -- start drag
sDragWidget = hit;
d->dragOffset = mousePos - thumbPos;
} else {
// Click on track -- jump to position
int32_t newVal = d->minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < d->minValue) { newVal = d->minValue; }
if (newVal > d->maxValue) { newVal = d->maxValue; }
d->value = newVal;
if (hit->onChange) {
hit->onChange(hit);
}
}
} else {
thumbRange = hit->w - SLIDER_THUMB_W;
thumbPos = ((d->value - d->minValue) * thumbRange) / range;
mousePos = vx - hit->x;
if (mousePos >= thumbPos && mousePos < thumbPos + SLIDER_THUMB_W) {
// Click on thumb -- start drag
sDragWidget = hit;
d->dragOffset = mousePos - thumbPos;
} else {
// Click on track -- jump to position
int32_t newVal = d->minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < d->minValue) { newVal = d->minValue; }
if (newVal > d->maxValue) { newVal = d->maxValue; }
d->value = newVal;
if (hit->onChange) {
hit->onChange(hit);
}
}
}
}
// ============================================================
// widgetSliderPaint
// ============================================================
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
SliderDataT *sd = (SliderDataT *)w->data;
uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg;
uint32_t tickFg = w->enabled ? fg : colors->windowShadow;
uint32_t thumbFg = w->enabled ? colors->buttonFace : colors->scrollbarTrough;
int32_t range = sd->maxValue - sd->minValue;
if (range <= 0) {
range = 1;
}
if (sd->vertical) {
// Track groove
int32_t trackX = w->x + (w->w - SLIDER_TRACK_H) / 2;
BevelStyleT groove;
groove.highlight = colors->windowShadow;
groove.shadow = colors->windowHighlight;
groove.face = colors->scrollbarTrough;
groove.width = 1;
drawBevel(d, ops, trackX, w->y, SLIDER_TRACK_H, w->h, &groove);
// Thumb
int32_t thumbRange = w->h - SLIDER_THUMB_W;
int32_t thumbY = w->y + ((sd->value - sd->minValue) * thumbRange) / range;
BevelStyleT thumb;
thumb.highlight = colors->windowHighlight;
thumb.shadow = colors->windowShadow;
thumb.face = thumbFg;
thumb.width = 2;
drawBevel(d, ops, w->x, thumbY, w->w, SLIDER_THUMB_W, &thumb);
// Center tick on thumb
drawHLine(d, ops, w->x + 3, thumbY + SLIDER_THUMB_W / 2, w->w - 6, tickFg);
} else {
// Track groove
int32_t trackY = w->y + (w->h - SLIDER_TRACK_H) / 2;
BevelStyleT groove;
groove.highlight = colors->windowShadow;
groove.shadow = colors->windowHighlight;
groove.face = colors->scrollbarTrough;
groove.width = 1;
drawBevel(d, ops, w->x, trackY, w->w, SLIDER_TRACK_H, &groove);
// Thumb
int32_t thumbRange = w->w - SLIDER_THUMB_W;
int32_t thumbX = w->x + ((sd->value - sd->minValue) * thumbRange) / range;
BevelStyleT thumb;
thumb.highlight = colors->windowHighlight;
thumb.shadow = colors->windowShadow;
thumb.face = thumbFg;
thumb.width = 2;
drawBevel(d, ops, thumbX, w->y, SLIDER_THUMB_W, w->h, &thumb);
// Center tick on thumb
drawVLine(d, ops, thumbX + SLIDER_THUMB_W / 2, w->y + 3, w->h - 6, tickFg);
}
if (w == sFocusedWidget) {
drawFocusRect(d, ops, w->x, w->y, w->w, w->h, fg);
}
}
// ============================================================
// widgetSliderOnDragUpdate
// ============================================================
//
// Called by the event loop during slider thumb drag. Computes the
// new value from mouse position and the stored drag offset.
static void widgetSliderOnDragUpdate(WidgetT *w, WidgetT *root, int32_t mouseX, int32_t mouseY) {
(void)root;
SliderDataT *d = (SliderDataT *)w->data;
int32_t range = d->maxValue - d->minValue;
if (range <= 0) {
return;
}
int32_t newVal;
if (d->vertical) {
int32_t thumbRange = w->h - SLIDER_THUMB_W;
int32_t relY = mouseY - w->y - d->dragOffset;
newVal = d->minValue + (relY * range) / thumbRange;
} else {
int32_t thumbRange = w->w - SLIDER_THUMB_W;
int32_t relX = mouseX - w->x - d->dragOffset;
newVal = d->minValue + (relX * range) / thumbRange;
}
newVal = clampInt(newVal, d->minValue, d->maxValue);
if (newVal != d->value) {
d->value = newVal;
if (w->onChange) {
w->onChange(w);
}
}
}
// ============================================================
// widgetSliderDestroy
// ============================================================
static void widgetSliderDestroy(WidgetT *w) {
free(w->data);
}
// ============================================================
// DXE registration
// ============================================================
static const WidgetClassT sClassSlider = {
.version = WGT_CLASS_VERSION,
.flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
.handlers = {
[WGT_METHOD_PAINT] = (void *)widgetSliderPaint,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetSliderCalcMinSize,
[WGT_METHOD_ON_MOUSE] = (void *)widgetSliderOnMouse,
[WGT_METHOD_ON_KEY] = (void *)widgetSliderOnKey,
[WGT_METHOD_DESTROY] = (void *)widgetSliderDestroy,
[WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetSliderOnDragUpdate,
}
};
// ============================================================
// Widget creation functions
// ============================================================
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal) {
WidgetT *w = widgetAlloc(parent, sTypeId);
if (w) {
SliderDataT *d = (SliderDataT *)calloc(1, sizeof(SliderDataT));
d->value = minVal;
d->minValue = minVal;
d->maxValue = maxVal;
w->data = d;
w->weight = 100;
}
return w;
}
int32_t wgtSliderGetValue(const WidgetT *w) {
VALIDATE_WIDGET(w, sTypeId, 0);
SliderDataT *d = (SliderDataT *)w->data;
return d->value;
}
void wgtSliderSetValue(WidgetT *w, int32_t value) {
VALIDATE_WIDGET_VOID(w, sTypeId);
SliderDataT *d = (SliderDataT *)w->data;
if (value < d->minValue) {
value = d->minValue;
}
if (value > d->maxValue) {
value = d->maxValue;
}
d->value = value;
wgtInvalidatePaint(w);
}
// ============================================================
// DXE registration
// ============================================================
static const struct {
WidgetT *(*create)(WidgetT *parent, int32_t minVal, int32_t maxVal);
void (*setValue)(WidgetT *w, int32_t value);
int32_t (*getValue)(const WidgetT *w);
} sApi = {
.create = wgtSlider,
.setValue = wgtSliderSetValue,
.getValue = wgtSliderGetValue
};
static const WgtPropDescT sProps[] = {
{ "Value", WGT_IFACE_INT, (void *)wgtSliderGetValue, (void *)wgtSliderSetValue, NULL }
};
static const WgtIfaceT sIface = {
.basName = "HScrollBar",
.props = sProps,
.propCount = 1,
.methods = NULL,
.methodCount = 0,
.events = NULL,
.eventCount = 0,
.createSig = WGT_CREATE_PARENT_INT_INT,
.createArgs = { 0, 100 },
.defaultEvent = "Change",
.namePrefix = "HScroll"
};
void wgtRegister(void) {
sTypeId = wgtRegisterClass(&sClassSlider);
wgtRegisterApi("slider", &sApi);
wgtRegisterIface("slider", &sIface);
}