#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; 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 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->focused) { 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 = { .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE, .paint = widgetSliderPaint, .paintOverlay = NULL, .calcMinSize = widgetSliderCalcMinSize, .layout = NULL, .onMouse = widgetSliderOnMouse, .onKey = widgetSliderOnKey, .destroy = widgetSliderDestroy, .getText = NULL, .setText = NULL, .onDragUpdate = 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 }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassSlider); wgtRegisterApi("slider", &sApi); }