399 lines
12 KiB
C
399 lines
12 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;
|
|
} 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;
|
|
hit->focused = true;
|
|
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
|
|
sDragSlider = hit;
|
|
sDragOffset = 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
|
|
sDragSlider = hit;
|
|
sDragOffset = 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->focused) {
|
|
drawFocusRect(d, ops, w->x, w->y, w->w, w->h, fg);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSliderScrollDragUpdate
|
|
// ============================================================
|
|
//
|
|
// Called by the event loop during slider thumb drag. Computes the
|
|
// new value from mouse position and the stored drag offset.
|
|
|
|
static void widgetSliderScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
|
|
(void)orient;
|
|
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 - dragOff;
|
|
newVal = d->minValue + (relY * range) / thumbRange;
|
|
} else {
|
|
int32_t thumbRange = w->w - SLIDER_THUMB_W;
|
|
int32_t relX = mouseX - w->x - dragOff;
|
|
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 = {
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE,
|
|
.paint = widgetSliderPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetSliderCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetSliderOnMouse,
|
|
.onKey = widgetSliderOnKey,
|
|
.destroy = widgetSliderDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL,
|
|
.scrollDragUpdate = widgetSliderScrollDragUpdate
|
|
};
|
|
|
|
// ============================================================
|
|
// 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
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassSlider);
|
|
wgtRegisterApi("slider", &sApi);
|
|
}
|