DVX_GUI/dvx/widgets/widgetSlider.c

296 lines
10 KiB
C

// 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 "widgetInternal.h"
// ============================================================
// wgtSlider
// ============================================================
// Default weight=100 so the slider stretches in its parent layout.
WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal) {
WidgetT *w = widgetAlloc(parent, WidgetSliderE);
if (w) {
w->as.slider.value = minVal;
w->as.slider.minValue = minVal;
w->as.slider.maxValue = maxVal;
w->as.slider.vertical = false;
w->weight = 100;
}
return w;
}
// ============================================================
// wgtSliderGetValue
// ============================================================
int32_t wgtSliderGetValue(const WidgetT *w) {
VALIDATE_WIDGET(w, WidgetSliderE, 0);
return w->as.slider.value;
}
// ============================================================
// wgtSliderSetValue
// ============================================================
void wgtSliderSetValue(WidgetT *w, int32_t value) {
VALIDATE_WIDGET_VOID(w, WidgetSliderE);
if (value < w->as.slider.minValue) {
value = w->as.slider.minValue;
}
if (value > w->as.slider.maxValue) {
value = w->as.slider.maxValue;
}
w->as.slider.value = value;
wgtInvalidatePaint(w);
}
// ============================================================
// 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;
if (w->as.slider.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;
int32_t step = 1;
int32_t range = w->as.slider.maxValue - w->as.slider.minValue;
if (range > 100) {
step = range / 100;
}
if (w->as.slider.vertical) {
if (key == (0x48 | 0x100)) {
w->as.slider.value -= step;
} else if (key == (0x50 | 0x100)) {
w->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
w->as.slider.value = w->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
w->as.slider.value = w->as.slider.maxValue;
} else {
return;
}
} else {
if (key == (0x4B | 0x100)) {
w->as.slider.value -= step;
} else if (key == (0x4D | 0x100)) {
w->as.slider.value += step;
} else if (key == (0x47 | 0x100)) {
w->as.slider.value = w->as.slider.minValue;
} else if (key == (0x4F | 0x100)) {
w->as.slider.value = w->as.slider.maxValue;
} else {
return;
}
}
w->as.slider.value = clampInt(w->as.slider.value, w->as.slider.minValue, w->as.slider.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;
int32_t range = hit->as.slider.maxValue - hit->as.slider.minValue;
if (range <= 0) {
return;
}
int32_t thumbRange;
int32_t thumbPos;
int32_t mousePos;
if (hit->as.slider.vertical) {
thumbRange = hit->h - SLIDER_THUMB_W;
thumbPos = ((hit->as.slider.value - hit->as.slider.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 = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }
if (newVal > hit->as.slider.maxValue) { newVal = hit->as.slider.maxValue; }
hit->as.slider.value = newVal;
if (hit->onChange) {
hit->onChange(hit);
}
}
} else {
thumbRange = hit->w - SLIDER_THUMB_W;
thumbPos = ((hit->as.slider.value - hit->as.slider.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 = hit->as.slider.minValue + ((mousePos - SLIDER_THUMB_W / 2) * range) / thumbRange;
if (newVal < hit->as.slider.minValue) { newVal = hit->as.slider.minValue; }
if (newVal > hit->as.slider.maxValue) { newVal = hit->as.slider.maxValue; }
hit->as.slider.value = newVal;
if (hit->onChange) {
hit->onChange(hit);
}
}
}
}
// ============================================================
// widgetSliderPaint
// ============================================================
// Paint: groove track centered in the cross-axis, thumb at value
// position. The thumb position formula is:
// thumbPos = ((value - minValue) * thumbRange) / range
// where thumbRange = widgetSize - SLIDER_THUMB_W.
// This maps value linearly to pixel position using integer math only.
// The groove is thin (SLIDER_TRACK_H = 4px) with a reversed bevel
// for the recessed look, while the thumb is the full widget height
// with a raised bevel for the 3D grab handle appearance.
void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)font;
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 = w->as.slider.maxValue - w->as.slider.minValue;
if (range <= 0) {
range = 1;
}
if (w->as.slider.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 + ((w->as.slider.value - w->as.slider.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 + ((w->as.slider.value - w->as.slider.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);
}
}