
/*
 * Until FreeBSD gets their act together;
 * http://www.mail-archive.com/freebsd-hackers@freebsd.org/msg69469.html
 */
#if defined( FREEBSD )
#   undef _XOPEN_SOURCE
#endif

#include "WINGsP.h"

#include <math.h>

/* undefine will disable the autoadjusting of the knob dimple to be
 * directly below the cursor
 * DOES NOT WORK */
#undef STRICT_NEXT_BEHAVIOUR

#define AUTOSCROLL_INITIAL_DELAY 	200

#define AUTOSCROLL_DELAY 		40

const char *WMScrollerDidScrollNotification = "WMScrollerDidScrollNotification";

typedef struct W_Scroller {
	W_Class widgetClass;
	W_View *view;

	void *clientData;
	WMAction *action;

	float knobProportion;
	float floatValue;

	WMHandlerID timerID;	/* for continuous scrolling mode */

#ifndef STRICT_NEXT_BEHAVIOUR
	int dragPoint;		/* point where the knob is being
				 * dragged */
#endif
	struct {
		WMScrollArrowPosition arrowsPosition:4;

		unsigned int horizontal:1;

		WMScrollerPart hitPart:4;

		/* */
		unsigned int documentFullyVisible:1;	/* document is fully visible */

		unsigned int prevSelected:1;

		unsigned int pushed:1;

		unsigned int incrDown:1;	/* whether increment button is down */

		unsigned int decrDown:1;

		unsigned int draggingKnob:1;

		unsigned int configured:1;

		unsigned int redrawPending:1;
	} flags;
} Scroller;

#define DEFAULT_ARROWS_POSITION	WSAMinEnd

#define BUTTON_SIZE             ((SCROLLER_WIDTH) - 4)

static void destroyScroller(Scroller * sPtr);
static void paintScroller(Scroller * sPtr);

static void willResizeScroller(W_ViewDelegate * self, WMView * view, unsigned int *width, unsigned int *height);
static void handleEvents(XEvent * event, void *data);
static void handleActionEvents(XEvent * event, void *data);

static void handleMotion(Scroller * sPtr, int mouseX, int mouseY);

W_ViewDelegate _ScrollerViewDelegate = {
	NULL,
	NULL,
	NULL,
	NULL,
	willResizeScroller
};

WMScroller *WMCreateScroller(WMWidget * parent)
{
	Scroller *sPtr;

	sPtr = wmalloc(sizeof(Scroller));
	sPtr->widgetClass = WC_Scroller;

	sPtr->view = W_CreateView(W_VIEW(parent));
	if (!sPtr->view) {
		wfree(sPtr);
		return NULL;
	}
	sPtr->view->self = sPtr;

	sPtr->view->delegate = &_ScrollerViewDelegate;

	sPtr->flags.documentFullyVisible = 1;

	WMCreateEventHandler(sPtr->view, ExposureMask | StructureNotifyMask
			     | ClientMessageMask, handleEvents, sPtr);

	W_ResizeView(sPtr->view, SCROLLER_WIDTH, SCROLLER_WIDTH);
	sPtr->flags.arrowsPosition = DEFAULT_ARROWS_POSITION;

	WMCreateEventHandler(sPtr->view, ButtonPressMask | ButtonReleaseMask
			     | EnterWindowMask | LeaveWindowMask | ButtonMotionMask, handleActionEvents, sPtr);

	sPtr->flags.hitPart = WSNoPart;

	sPtr->floatValue = 0.0;
	sPtr->knobProportion = 1.0;

	return sPtr;
}

void WMSetScrollerArrowsPosition(WMScroller * sPtr, WMScrollArrowPosition position)
{
	sPtr->flags.arrowsPosition = position;
	if (sPtr->view->flags.realized) {
		paintScroller(sPtr);
	}
}

static void willResizeScroller(W_ViewDelegate * self, WMView * view, unsigned int *width, unsigned int *height)
{
	WMScroller *sPtr = (WMScroller *) view->self;

	/* Parameter not used, but tell the compiler that it is ok */
	(void) self;

	if (*width > *height) {
		sPtr->flags.horizontal = 1;
		*height = SCROLLER_WIDTH;
	} else {
		sPtr->flags.horizontal = 0;
		*width = SCROLLER_WIDTH;
	}
}

void WMSetScrollerAction(WMScroller * sPtr, WMAction * action, void *clientData)
{
	CHECK_CLASS(sPtr, WC_Scroller);

	sPtr->action = action;

	sPtr->clientData = clientData;
}

void WMSetScrollerParameters(WMScroller * sPtr, float floatValue, float knobProportion)
{
	/*
	 * This value represents 1 pixel on a 4k wide screen, it makes
	 * a good minimum; this ensure a non-null value to avoid
	 * potential division-by-0.
	 * Please note that there is another size check when drawing
	 * the knob to make sure it will remain selectable.
	 */
	static const float min_knob_proportion = 1.0 / 4096.0;

	CHECK_CLASS(sPtr, WC_Scroller);

	assert(!isnan(floatValue));

	if (floatValue < 0.0F)
		sPtr->floatValue = 0.0F;
	else if (floatValue > 1.0F)
		sPtr->floatValue = 1.0F;
	else
		sPtr->floatValue = floatValue;

	if (knobProportion <= min_knob_proportion) {

		sPtr->knobProportion = min_knob_proportion;
		sPtr->flags.documentFullyVisible = 0;

	} else if (knobProportion >= 1.0F) {

		sPtr->knobProportion = 1.0F;
		sPtr->flags.documentFullyVisible = 1;

	} else {
		sPtr->knobProportion = knobProportion;
		sPtr->flags.documentFullyVisible = 0;
	}

	if (sPtr->view->flags.realized)
		paintScroller(sPtr);

	/*   WMPostNotificationName(WMScrollerDidScrollNotification, sPtr, NULL); */
}

float WMGetScrollerKnobProportion(WMScroller * sPtr)
{
	CHECK_CLASS(sPtr, WC_Scroller);

	return sPtr->knobProportion;
}

float WMGetScrollerValue(WMScroller * sPtr)
{
	CHECK_CLASS(sPtr, WC_Scroller);

	return sPtr->floatValue;
}

WMScrollerPart WMGetScrollerHitPart(WMScroller * sPtr)
{
	CHECK_CLASS(sPtr, WC_Scroller);

	return sPtr->flags.hitPart;
}

static void paintArrow(WMScroller * sPtr, Drawable d, int part)
/*
 * part- 0 paints the decrement arrow, 1 the increment arrow
 */
{
	WMView *view = sPtr->view;
	WMScreen *scr = view->screen;
	int ofs;
	W_Pixmap *arrow;

#ifndef DOUBLE_BUFFER
	GC gc = scr->lightGC;
#endif

	if (part == 0) {	/* decrement button */
		if (sPtr->flags.horizontal) {
			if (sPtr->flags.arrowsPosition == WSAMaxEnd) {
				ofs = view->size.width - 2 * (BUTTON_SIZE + 1) - 1;
			} else {
				ofs = 2;
			}
			if (sPtr->flags.decrDown)
				arrow = scr->hiLeftArrow;
			else
				arrow = scr->leftArrow;

		} else {
			if (sPtr->flags.arrowsPosition == WSAMaxEnd) {
				ofs = view->size.height - 2 * (BUTTON_SIZE + 1) - 1;
			} else {
				ofs = 2;
			}
			if (sPtr->flags.decrDown)
				arrow = scr->hiUpArrow;
			else
				arrow = scr->upArrow;
		}

#ifndef DOUBLE_BUFFER
		if (sPtr->flags.decrDown)
			gc = WMColorGC(scr->white);
#endif
	} else {		/* increment button */
		if (sPtr->flags.horizontal) {
			if (sPtr->flags.arrowsPosition == WSAMaxEnd) {
				ofs = view->size.width - BUTTON_SIZE + 1 - 3;
			} else {
				ofs = 2 + BUTTON_SIZE + 1;
			}
			if (sPtr->flags.incrDown)
				arrow = scr->hiRightArrow;
			else
				arrow = scr->rightArrow;
		} else {
			if (sPtr->flags.arrowsPosition == WSAMaxEnd) {
				ofs = view->size.height - BUTTON_SIZE + 1 - 3;
			} else {
				ofs = 2 + BUTTON_SIZE + 1;
			}
			if (sPtr->flags.incrDown)
				arrow = scr->hiDownArrow;
			else
				arrow = scr->downArrow;
		}

#ifndef DOUBLE_BUFFER
		if (sPtr->flags.incrDown)
			gc = scr->whiteGC;
#endif
	}

	if (sPtr->flags.horizontal) {
		/* paint button */
#ifndef DOUBLE_BUFFER
		XFillRectangle(scr->display, d, gc, ofs + 1, 2 + 1, BUTTON_SIZE + 1 - 3, BUTTON_SIZE - 3);
#else
		if ((!part && sPtr->flags.decrDown) || (part && sPtr->flags.incrDown))
			XFillRectangle(scr->display, d, WMColorGC(scr->white),
				       ofs + 1, 2 + 1, BUTTON_SIZE + 1 - 3, BUTTON_SIZE - 3);
#endif				/* DOUBLE_BUFFER */
		W_DrawRelief(scr, d, ofs, 2, BUTTON_SIZE, BUTTON_SIZE, WRRaised);

		/* paint arrow */
		XSetClipMask(scr->display, scr->clipGC, arrow->mask);
		XSetClipOrigin(scr->display, scr->clipGC,
			       ofs + (BUTTON_SIZE - arrow->width) / 2, 2 + (BUTTON_SIZE - arrow->height) / 2);

		XCopyArea(scr->display, arrow->pixmap, d, scr->clipGC,
			  0, 0, arrow->width, arrow->height,
			  ofs + (BUTTON_SIZE - arrow->width) / 2, 2 + (BUTTON_SIZE - arrow->height) / 2);

	} else {		/* vertical */

		/* paint button */
#ifndef DOUBLE_BUFFER
		XFillRectangle(scr->display, d, gc, 2 + 1, ofs + 1, BUTTON_SIZE - 3, BUTTON_SIZE + 1 - 3);
#else
		if ((!part && sPtr->flags.decrDown) || (part && sPtr->flags.incrDown))
			XFillRectangle(scr->display, d, WMColorGC(scr->white),
				       2 + 1, ofs + 1, BUTTON_SIZE - 3, BUTTON_SIZE + 1 - 3);
#endif				/* DOUBLE_BUFFER */
		W_DrawRelief(scr, d, 2, ofs, BUTTON_SIZE, BUTTON_SIZE, WRRaised);

		/* paint arrow */

		XSetClipMask(scr->display, scr->clipGC, arrow->mask);
		XSetClipOrigin(scr->display, scr->clipGC,
			       2 + (BUTTON_SIZE - arrow->width) / 2, ofs + (BUTTON_SIZE - arrow->height) / 2);
		XCopyArea(scr->display, arrow->pixmap, d, scr->clipGC,
			  0, 0, arrow->width, arrow->height,
			  2 + (BUTTON_SIZE - arrow->width) / 2, ofs + (BUTTON_SIZE - arrow->height) / 2);
	}
}

static int knobLength(Scroller * sPtr)
{
	int tmp, length;

	if (sPtr->flags.horizontal)
		length = sPtr->view->size.width - 4;
	else
		length = sPtr->view->size.height - 4;

	if (sPtr->flags.arrowsPosition != WSANone) {
		length -= 2 * (BUTTON_SIZE + 1);
	}

	tmp = (int)((float)length * sPtr->knobProportion + 0.5F);
	/* keep minimum size */
	if (tmp < BUTTON_SIZE)
		tmp = BUTTON_SIZE;

	return tmp;
}

static void paintScroller(Scroller * sPtr)
{
	WMView *view = sPtr->view;
	WMScreen *scr = view->screen;
#ifdef DOUBLE_BUFFER
	Pixmap d;
#else
	Drawable d = view->window;
#endif
	int length, ofs;
	float knobP, knobL;

#ifdef DOUBLE_BUFFER
	d = XCreatePixmap(scr->display, view->window, view->size.width, view->size.height, scr->depth);
	XFillRectangle(scr->display, d, WMColorGC(scr->gray), 0, 0, view->size.width, view->size.height);
#endif

	XDrawRectangle(scr->display, d, WMColorGC(scr->black), 0, 0, view->size.width - 1, view->size.height - 1);
#ifndef DOUBLE_BUFFER
	XDrawRectangle(scr->display, d, WMColorGC(scr->gray), 1, 1, view->size.width - 3, view->size.height - 3);
#endif

	if (sPtr->flags.horizontal)
		length = view->size.width - 4;
	else
		length = view->size.height - 4;

	if (sPtr->flags.documentFullyVisible) {
		XFillRectangle(scr->display, d, scr->stippleGC, 2, 2, view->size.width - 4, view->size.height - 4);
	} else {
		ofs = 2;
		if (sPtr->flags.arrowsPosition == WSAMaxEnd) {
			length -= (BUTTON_SIZE + 1) * 2;
		} else if (sPtr->flags.arrowsPosition == WSAMinEnd) {
			ofs += (BUTTON_SIZE + 1) * 2;
			length -= (BUTTON_SIZE + 1) * 2;
		}

		knobL = (float)knobLength(sPtr);

		knobP = sPtr->floatValue * ((float)length - knobL);

		if (sPtr->flags.horizontal) {
			/* before */
			XFillRectangle(scr->display, d, scr->stippleGC, ofs, 2, (int)knobP, view->size.height - 4);

			/* knob */
#ifndef DOUBLE_BUFFER
			XFillRectangle(scr->display, d, scr->lightGC,
				       ofs + (int)knobP + 2, 2 + 2, (int)knobL - 4, view->size.height - 4 - 4);
#endif
			W_DrawRelief(scr, d, ofs + (int)knobP, 2, (int)knobL, view->size.height - 4, WRRaised);

			XCopyArea(scr->display, scr->scrollerDimple->pixmap, d,
				  scr->copyGC, 0, 0,
				  scr->scrollerDimple->width, scr->scrollerDimple->height,
				  ofs + (int)knobP + ((int)knobL - scr->scrollerDimple->width - 1) / 2,
				  (view->size.height - scr->scrollerDimple->height - 1) / 2);

			/* after */
			if ((int)(knobP + knobL) < length)
				XFillRectangle(scr->display, d, scr->stippleGC,
					       ofs + (int)(knobP + knobL), 2,
					       length - (int)(knobP + knobL), view->size.height - 4);
		} else {
			/* before */
			if (knobP > 0.0F)
				XFillRectangle(scr->display, d, scr->stippleGC,
					       2, ofs, view->size.width - 4, (int)knobP);

			/* knob */
#ifndef DOUBLE_BUFFER
			XFillRectangle(scr->display, d, scr->lightGC,
				       2 + 2, ofs + (int)knobP + 2, view->size.width - 4 - 4, (int)knobL - 4);
#endif
			XCopyArea(scr->display, scr->scrollerDimple->pixmap, d,
				  scr->copyGC, 0, 0,
				  scr->scrollerDimple->width, scr->scrollerDimple->height,
				  (view->size.width - scr->scrollerDimple->width - 1) / 2,
				  ofs + (int)knobP + ((int)knobL - scr->scrollerDimple->height - 1) / 2);

			W_DrawRelief(scr, d, 2, ofs + (int)knobP, view->size.width - 4, (int)knobL, WRRaised);

			/* after */
			if ((int)(knobP + knobL) < length)
				XFillRectangle(scr->display, d, scr->stippleGC,
					       2, ofs + (int)(knobP + knobL),
					       view->size.width - 4, length - (int)(knobP + knobL));
		}

		if (sPtr->flags.arrowsPosition != WSANone) {
			paintArrow(sPtr, d, 0);
			paintArrow(sPtr, d, 1);
		}
	}

#ifdef DOUBLE_BUFFER
	XCopyArea(scr->display, d, view->window, scr->copyGC, 0, 0, view->size.width, view->size.height, 0, 0);
	XFreePixmap(scr->display, d);
#endif
}

static void handleEvents(XEvent * event, void *data)
{
	Scroller *sPtr = (Scroller *) data;

	CHECK_CLASS(data, WC_Scroller);

	switch (event->type) {
	case Expose:
		if (event->xexpose.count == 0)
			paintScroller(sPtr);
		break;

	case DestroyNotify:
		destroyScroller(sPtr);
		break;
	}
}

/*
 * locatePointInScroller-
 *     Return the part of the scroller where the point is located.
 */
static WMScrollerPart locatePointInScroller(Scroller * sPtr, int x, int y, int alternate)
{
	int width = sPtr->view->size.width;
	int height = sPtr->view->size.height;
	int c, p1, p2, p3, p4, p5, p6;
	int knobL, slotL;

	/* if there is no knob... */
	if (sPtr->flags.documentFullyVisible)
		return WSKnobSlot;

	if (sPtr->flags.horizontal)
		c = x;
	else
		c = y;

	/*     p1  p2           p3           p4   p5   p6
	 * |   |   |###########|             |#####|   |   |
	 * | < | > |###########|      O      |#####| < | > |
	 * |   |   |###########|             |#####|   |   |
	 */

	if (sPtr->flags.arrowsPosition == WSAMinEnd) {
		p1 = 18;
		p2 = 36;

		if (sPtr->flags.horizontal) {
			slotL = width - 36;
			p5 = width;
		} else {
			slotL = height - 36;
			p5 = height;
		}
		p6 = p5;
	} else if (sPtr->flags.arrowsPosition == WSAMaxEnd) {
		if (sPtr->flags.horizontal) {
			slotL = width - 36;
			p6 = width - 18;
		} else {
			slotL = height - 36;
			p6 = height - 18;
		}
		p5 = p6 - 18;

		p1 = p2 = 0;
	} else {
		/* no arrows */
		p1 = p2 = 0;

		if (sPtr->flags.horizontal) {
			slotL = p5 = p6 = width;
		} else {
			slotL = p5 = p6 = height;
		}
	}

	knobL = knobLength(sPtr);
	p3 = p2 + (int)((float)(slotL - knobL) * sPtr->floatValue);
	p4 = p3 + knobL;

	/* uses a mix of the NS and Win ways of doing scroll page */
	if (c <= p1)
		return alternate ? WSDecrementPage : WSDecrementLine;
	else if (c <= p2)
		return alternate ? WSIncrementPage : WSIncrementLine;
	else if (c <= p3)
		return WSDecrementPage;
	else if (c <= p4)
		return WSKnob;
	else if (c <= p5)
		return WSIncrementPage;
	else if (c <= p6)
		return alternate ? WSDecrementPage : WSDecrementLine;
	else
		return alternate ? WSIncrementPage : WSIncrementLine;
}

static void handlePush(Scroller * sPtr, int pushX, int pushY, int alternate)
{
	WMScrollerPart part;
	int doAction = 0;

	part = locatePointInScroller(sPtr, pushX, pushY, alternate);

	sPtr->flags.hitPart = part;

	switch (part) {
	case WSIncrementLine:
		sPtr->flags.incrDown = 1;
		doAction = 1;
		break;

	case WSIncrementPage:
		doAction = 1;
		break;

	case WSDecrementLine:
		sPtr->flags.decrDown = 1;
		doAction = 1;
		break;

	case WSDecrementPage:
		doAction = 1;
		break;

	case WSKnob:
		sPtr->flags.draggingKnob = 1;
#ifndef STRICT_NEXT_BEHAVIOUR
		if (sPtr->flags.horizontal)
			sPtr->dragPoint = pushX;
		else
			sPtr->dragPoint = pushY;

		{
			int length, knobP;
			int buttonsLen;

			if (sPtr->flags.arrowsPosition != WSANone)
				buttonsLen = 2 * (BUTTON_SIZE + 1);
			else
				buttonsLen = 0;

			if (sPtr->flags.horizontal)
				length = sPtr->view->size.width - 4 - buttonsLen;
			else
				length = sPtr->view->size.height - 4 - buttonsLen;

			knobP = (int)(sPtr->floatValue * (float)(length - knobLength(sPtr)));

			if (sPtr->flags.arrowsPosition == WSAMinEnd)
				sPtr->dragPoint -= 2 + buttonsLen + knobP;
			else
				sPtr->dragPoint -= 2 + knobP;
		}
#endif				/* STRICT_NEXT_BEHAVIOUR */
		/* This does not seem necesary here since we don't know yet if the
		 * knob will be dragged later. -Dan
		 handleMotion(sPtr, pushX, pushY); */
		break;

	case WSDecrementWheel:
	case WSIncrementWheel:
	case WSKnobSlot:
	case WSNoPart:
		/* dummy */
		break;
	}

	if (doAction && sPtr->action) {
		(*sPtr->action) (sPtr, sPtr->clientData);

		WMPostNotificationName(WMScrollerDidScrollNotification, sPtr, NULL);
	}
}

static float floatValueForPoint(Scroller * sPtr, int point)
{
	float floatValue = 0;
	float position;
	int slotOfs, slotLength, knobL;

	if (sPtr->flags.horizontal)
		slotLength = sPtr->view->size.width - 4;
	else
		slotLength = sPtr->view->size.height - 4;

	slotOfs = 2;
	if (sPtr->flags.arrowsPosition == WSAMaxEnd) {
		slotLength -= (BUTTON_SIZE + 1) * 2;
	} else if (sPtr->flags.arrowsPosition == WSAMinEnd) {
		slotOfs += (BUTTON_SIZE + 1) * 2;
		slotLength -= (BUTTON_SIZE + 1) * 2;
	}

	knobL = (float)knobLength(sPtr);
#ifdef STRICT_NEXT_BEHAVIOUR
	if (point < slotOfs + knobL / 2)
		position = (float)(slotOfs + knobL / 2);
	else if (point > slotOfs + slotLength - knobL / 2)
		position = (float)(slotOfs + slotLength - knobL / 2);
	else
		position = (float)point;

	floatValue = (position - (float)(slotOfs + slotLength / 2))
	    / (float)(slotLength - knobL);
#else
	/* Adjust the last point to lie inside the knob slot */
	if (point < slotOfs)
		position = (float)slotOfs;
	else if (point > slotOfs + slotLength)
		position = (float)(slotOfs + slotLength);
	else
		position = (float)point;

	/* Compute the float value */
	floatValue = (position - (float)slotOfs) / (float)(slotLength - knobL);
#endif

	assert(!isnan(floatValue));
	return floatValue;
}

static void handleMotion(Scroller * sPtr, int mouseX, int mouseY)
{
	if (sPtr->flags.draggingKnob) {
		float newFloatValue;
		int point;

		if (sPtr->flags.horizontal) {
			point = mouseX;
		} else {
			point = mouseY;
		}

#ifndef STRICT_NEXT_BEHAVIOUR
		point -= sPtr->dragPoint;
#endif

		newFloatValue = floatValueForPoint(sPtr, point);
		WMSetScrollerParameters(sPtr, newFloatValue, sPtr->knobProportion);
		if (sPtr->action) {
			(*sPtr->action) (sPtr, sPtr->clientData);
			WMPostNotificationName(WMScrollerDidScrollNotification, sPtr, NULL);
		}
	} else {
		int part;

		part = locatePointInScroller(sPtr, mouseX, mouseY, False);

		sPtr->flags.hitPart = part;

		if (part == WSIncrementLine && sPtr->flags.decrDown) {
			sPtr->flags.decrDown = 0;
			sPtr->flags.incrDown = 1;
		} else if (part == WSDecrementLine && sPtr->flags.incrDown) {
			sPtr->flags.incrDown = 0;
			sPtr->flags.decrDown = 1;
		} else if (part != WSIncrementLine && part != WSDecrementLine) {
			sPtr->flags.incrDown = 0;
			sPtr->flags.decrDown = 0;
		}
	}
}

static void autoScroll(void *clientData)
{
	Scroller *sPtr = (Scroller *) clientData;

	if (sPtr->action) {
		(*sPtr->action) (sPtr, sPtr->clientData);
		WMPostNotificationName(WMScrollerDidScrollNotification, sPtr, NULL);
	}
	sPtr->timerID = WMAddTimerHandler(AUTOSCROLL_DELAY, autoScroll, clientData);
}

static void handleActionEvents(XEvent * event, void *data)
{
	Scroller *sPtr = (Scroller *) data;
	int wheelDecrement, wheelIncrement;
	int id, dd;

	/* check if we're really dealing with a scroller, as something
	 * might have gone wrong in the event dispatching stuff */
	CHECK_CLASS(sPtr, WC_Scroller);

	id = sPtr->flags.incrDown;
	dd = sPtr->flags.decrDown;

	switch (event->type) {
	case EnterNotify:
		break;

	case LeaveNotify:
		if (sPtr->timerID) {
			WMDeleteTimerHandler(sPtr->timerID);
			sPtr->timerID = NULL;
		}
		sPtr->flags.incrDown = 0;
		sPtr->flags.decrDown = 0;
		break;

	case ButtonPress:
		/* FIXME: change Mod1Mask with something else */
		if (sPtr->flags.documentFullyVisible)
			break;

		if (sPtr->flags.horizontal) {
			wheelDecrement = WINGsConfiguration.mouseWheelDown;
			wheelIncrement = WINGsConfiguration.mouseWheelUp;
		} else {
			wheelDecrement = WINGsConfiguration.mouseWheelUp;
			wheelIncrement = WINGsConfiguration.mouseWheelDown;
		}

		if (event->xbutton.button == wheelDecrement) {
			if (event->xbutton.state & ControlMask) {
				sPtr->flags.hitPart = WSDecrementPage;
			} else if (event->xbutton.state & ShiftMask) {
				sPtr->flags.hitPart = WSDecrementLine;
			} else {
				sPtr->flags.hitPart = WSDecrementWheel;
			}
			if (sPtr->action) {
				(*sPtr->action) (sPtr, sPtr->clientData);
				WMPostNotificationName(WMScrollerDidScrollNotification, sPtr, NULL);
			}
		} else if (event->xbutton.button == wheelIncrement) {
			if (event->xbutton.state & ControlMask) {
				sPtr->flags.hitPart = WSIncrementPage;
			} else if (event->xbutton.state & ShiftMask) {
				sPtr->flags.hitPart = WSIncrementLine;
			} else {
				sPtr->flags.hitPart = WSIncrementWheel;
			}
			if (sPtr->action) {
				(*sPtr->action) (sPtr, sPtr->clientData);
				WMPostNotificationName(WMScrollerDidScrollNotification, sPtr, NULL);
			}
		} else {
			handlePush(sPtr, event->xbutton.x, event->xbutton.y, (event->xbutton.state & Mod1Mask)
				   || event->xbutton.button == Button2);
			/* continue scrolling if pushed on the buttons */
			if (sPtr->flags.hitPart == WSIncrementLine || sPtr->flags.hitPart == WSDecrementLine) {
				sPtr->timerID = WMAddTimerHandler(AUTOSCROLL_INITIAL_DELAY, autoScroll, sPtr);
			}
		}
		break;

	case ButtonRelease:
		if (sPtr->flags.draggingKnob) {
			if (sPtr->action) {
				(*sPtr->action) (sPtr, sPtr->clientData);
				WMPostNotificationName(WMScrollerDidScrollNotification, sPtr, NULL);
			}
		}
		if (sPtr->timerID) {
			WMDeleteTimerHandler(sPtr->timerID);
			sPtr->timerID = NULL;
		}
		sPtr->flags.incrDown = 0;
		sPtr->flags.decrDown = 0;
		sPtr->flags.draggingKnob = 0;
		break;

	case MotionNotify:
		handleMotion(sPtr, event->xbutton.x, event->xbutton.y);
		if (sPtr->timerID && sPtr->flags.hitPart != WSIncrementLine
		    && sPtr->flags.hitPart != WSDecrementLine) {
			WMDeleteTimerHandler(sPtr->timerID);
			sPtr->timerID = NULL;
		}
		break;
	}
	if (id != sPtr->flags.incrDown || dd != sPtr->flags.decrDown)
		paintScroller(sPtr);
}

static void destroyScroller(Scroller * sPtr)
{
	/* we don't want autoscroll try to scroll a freed widget */
	if (sPtr->timerID) {
		WMDeleteTimerHandler(sPtr->timerID);
	}

	wfree(sPtr);
}
