#define DVX_WIDGET_IMPL // widgetButton.c -- Button widget // // Standard push button with text label, Motif-style 2px beveled border, // and press animation. The button uses a two-phase press model: // - Mouse press: sets pressed=true and stores the widget in sPressedButton. // The event dispatcher tracks mouse movement -- if the mouse leaves the // button bounds, pressed is cleared (visual feedback), and if it re-enters, // pressed is re-set. The onClick callback fires only on mouse-up while // still inside the button. This gives the user a chance to cancel by // dragging away, matching standard GUI behavior. // - Keyboard press (Space/Enter): sets pressed=true and stores in // sKeyPressedBtn. The onClick fires on key-up. This uses a different // global than mouse press because key and mouse can't happen simultaneously. // // The press animation shifts text and focus rect by BUTTON_PRESS_OFFSET (1px) // down and right, and swaps the bevel highlight/shadow colors to create the // illusion of the button being pushed "into" the screen. // // Text supports accelerator keys via '&' prefix (e.g., "&OK" underlines 'O' // and Alt+O activates the button). #include "dvxWidgetPlugin.h" #define BUTTON_PAD_H 8 #define BUTTON_PAD_V 4 #define BUTTON_FOCUS_INSET 3 #define BUTTON_PRESS_OFFSET 1 static int32_t sTypeId = -1; typedef struct { const char *text; bool pressed; } ButtonDataT; // ============================================================ // Prototypes // ============================================================ static void widgetButtonDestroy(WidgetT *w); // ============================================================ // widgetButtonAccelActivate // ============================================================ void widgetButtonAccelActivate(WidgetT *w, WidgetT *root) { (void)root; ButtonDataT *d = (ButtonDataT *)w->data; d->pressed = true; sKeyPressedBtn = w; wgtInvalidatePaint(w); } // ============================================================ // widgetButtonCalcMinSize // ============================================================ // Min size includes horizontal and vertical padding around the text. The text // width is computed by textWidthAccel which excludes the '&' accelerator prefix // character from the measurement. void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { ButtonDataT *d = (ButtonDataT *)w->data; w->calcMinW = textWidthAccel(font, d->text) + BUTTON_PAD_H * 2; w->calcMinH = font->charHeight + BUTTON_PAD_V * 2; } // ============================================================ // widgetButtonDestroy // ============================================================ static void widgetButtonDestroy(WidgetT *w) { free(w->data); w->data = NULL; } // ============================================================ // widgetButtonGetText // ============================================================ const char *widgetButtonGetText(const WidgetT *w) { ButtonDataT *d = (ButtonDataT *)w->data; return d->text; } // ============================================================ // widgetButtonOnKey // ============================================================ void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod) { (void)mod; if (key == ' ' || key == 0x0D) { ButtonDataT *d = (ButtonDataT *)w->data; d->pressed = true; sKeyPressedBtn = w; wgtInvalidatePaint(w); } } // ============================================================ // widgetButtonOnMouse // ============================================================ void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vx; (void)vy; ButtonDataT *d = (ButtonDataT *)w->data; w->focused = true; d->pressed = true; sDragWidget = w; } // ============================================================ // widgetButtonPaint // ============================================================ // Paint: draws the beveled border, centered text, and optional focus rect. // When pressed, the bevel colors swap (highlight<->shadow) creating the sunken // appearance, and the text shifts by BUTTON_PRESS_OFFSET pixels. Disabled // buttons use the "embossed" text technique (highlight color at +1,+1, then // shadow color at 0,0) to create a chiseled/etched look -- this is the // standard Windows 3.1 disabled control appearance. void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { ButtonDataT *bd = (ButtonDataT *)w->data; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; BevelStyleT bevel; bevel.highlight = bd->pressed ? colors->windowShadow : colors->windowHighlight; bevel.shadow = bd->pressed ? colors->windowHighlight : colors->windowShadow; bevel.face = bgFace; bevel.width = 2; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); int32_t textW = textWidthAccel(font, bd->text); int32_t textX = w->x + (w->w - textW) / 2; int32_t textY = w->y + (w->h - font->charHeight) / 2; if (bd->pressed) { textX += BUTTON_PRESS_OFFSET; textY += BUTTON_PRESS_OFFSET; } if (!w->enabled) { drawTextAccelEmbossed(d, ops, font, textX, textY, bd->text, colors); } else { drawTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true); } if (w->focused) { int32_t off = bd->pressed ? BUTTON_PRESS_OFFSET : 0; drawFocusRect(d, ops, w->x + BUTTON_FOCUS_INSET + off, w->y + BUTTON_FOCUS_INSET + off, w->w - BUTTON_FOCUS_INSET * 2, w->h - BUTTON_FOCUS_INSET * 2, fg); } } // ============================================================ // widgetButtonOnDragEnd // ============================================================ static void widgetButtonOnDragEnd(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { ButtonDataT *d = (ButtonDataT *)w->data; d->pressed = false; if (w->window == root->window && x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h) { if (w->onClick) { w->onClick(w); } } wgtInvalidatePaint(w); } // ============================================================ // widgetButtonOnDragUpdate // ============================================================ static void widgetButtonOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { ButtonDataT *d = (ButtonDataT *)w->data; bool over = (w->window == root->window && x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h); d->pressed = over; wgtInvalidatePaint(w); } // ============================================================ // widgetButtonSetText // ============================================================ void widgetButtonSetText(WidgetT *w, const char *text) { ButtonDataT *d = (ButtonDataT *)w->data; d->text = text; w->accelKey = accelParse(text); } // ============================================================ // DXE registration // ============================================================ static const WidgetClassT sClassButton = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE, .handlers = { [WGT_METHOD_PAINT] = (void *)widgetButtonPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetButtonCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)widgetButtonOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetButtonOnKey, [WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetButtonAccelActivate, [WGT_METHOD_DESTROY] = (void *)widgetButtonDestroy, [WGT_METHOD_GET_TEXT] = (void *)widgetButtonGetText, [WGT_METHOD_SET_TEXT] = (void *)widgetButtonSetText, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetButtonOnDragUpdate, [WGT_METHOD_ON_DRAG_END] = (void *)widgetButtonOnDragEnd, } }; // ============================================================ // Widget creation functions // ============================================================ WidgetT *wgtButton(WidgetT *parent, const char *text) { WidgetT *w = widgetAlloc(parent, sTypeId); if (w) { ButtonDataT *d = calloc(1, sizeof(ButtonDataT)); w->data = d; d->text = text; w->accelKey = accelParse(text); } return w; } // ============================================================ // DXE registration // ============================================================ static const struct { WidgetT *(*create)(WidgetT *parent, const char *text); } sApi = { .create = wgtButton }; static const WgtIfaceT sIface = { .basName = "CommandButton", .props = NULL, .propCount = 0, .methods = NULL, .methodCount = 0, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_TEXT, .defaultEvent = "Click", .namePrefix = "Command" }; void wgtRegister(void) { sTypeId = wgtRegisterClass(&sClassButton); wgtRegisterApi("button", &sApi); wgtRegisterIface("button", &sIface); }