/*
 * Beryl Input-Enabled-Zoom-Plugin
 *
 * Copyright : (C) 2006 by Dennis Kasprzyk
 * E-mail    : onestone@beryl-project.org
 *
 * Copyright : (C) 2006 Quinn Storm
 * E-mail    : quinnstorm@beryl-project.org
 *
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>

#include <beryl.h>

#define ZOOM_IN_BUTTON_DEFAULT    Button4
#define ZOOM_IN_MODIFIERS_DEFAULT CompSuperMask

#define ZOOM_OUT_BUTTON_DEFAULT    Button5
#define ZOOM_OUT_MODIFIERS_DEFAULT CompSuperMask

#define ZOOM_SPEED_DEFAULT   1.00f
#define ZOOM_SPEED_MIN       0.1f
#define ZOOM_SPEED_MAX       4.0f
#define ZOOM_SPEED_PRECISION 0.1f

#define ZOOM_STEP_DEFAULT   1.25f
#define ZOOM_STEP_MIN       1.05f
#define ZOOM_STEP_MAX       5.0f
#define ZOOM_STEP_PRECISION 0.05f

#define ZOOM_MAX_FACTOR_DEFAULT   16.0f
#define ZOOM_MAX_FACTOR_MIN       1.1f
#define ZOOM_MAX_FACTOR_MAX       64.0f
#define ZOOM_MAX_FACTOR_PRECISION 0.1f

#define ZOOM_TIMESTEP_DEFAULT   1.2f
#define ZOOM_TIMESTEP_MIN       0.1f
#define ZOOM_TIMESTEP_MAX       20.0f
#define ZOOM_TIMESTEP_PRECISION 0.1f

#define ZOOM_MOUSE_UPDATE_MIN     1
#define ZOOM_MOUSE_UPDATE_MAX     500
#define ZOOM_MOUSE_UPDATE_DEFAULT 10

#define ZOOM_FILTER_LINEAR_DEFAULT TRUE

#define ZOOM_HIDE_NORMAL_CURSOR_DEFAULT FALSE
#define ZOOM_SHOW_SCALED_CURSOR_DEFAULT FALSE

#define ZOOM_OUT_ON_CUBE_DEFAULT TRUE

static int displayPrivateIndex;

#define ZOOM_DISPLAY_OPTION_IN         0
#define ZOOM_DISPLAY_OPTION_OUT         1
#define ZOOM_DISPLAY_OPTION_NUM         2

typedef struct _ZoomDisplay
{
	int screenPrivateIndex;
	HandleEventProc handleEvent;

	Bool fixesSupported;
	int fixesEventBase;
	int fixesErrorBase;
	Bool canHideCursor;

	CompOption opt[ZOOM_DISPLAY_OPTION_NUM];
} ZoomDisplay;

#define ZOOM_SCREEN_OPTION_SPEED              0
#define ZOOM_SCREEN_OPTION_STEP               1
#define ZOOM_SCREEN_OPTION_MAX_FACTOR         2
#define ZOOM_SCREEN_OPTION_TIMESTEP           3
#define ZOOM_SCREEN_OPTION_FILTER_LINEAR      4
#define ZOOM_SCREEN_OPTION_MOUSE_UPDATE       5
#define ZOOM_SCREEN_OPTION_OUT_ON_CUBE        6
#define ZOOM_SCREEN_OPTION_NUM                7
//#define ZOOM_SCREEN_OPTION_HIDE_NORMAL_CURSOR 5
//#define ZOOM_SCREEN_OPTION_SHOW_SCALED_CURSOR 6


typedef struct _CursorTexture
{
	Bool isSet;
	GLuint texture;
	int width;
	int height;
	int hotX;
	int hotY;
} CursorTexture;

typedef struct _ZoomScreen
{
	PreparePaintScreenProc preparePaintScreen;
	DonePaintScreenProc donePaintScreen;
	PaintScreenProc paintScreen;
	PaintTransformedScreenProc paintTransformedScreen;
	SetScreenOptionForPluginProc setScreenOptionForPlugin;
	ApplyScreenTransformProc applyScreenTransform;

	CompTimeoutHandle mouseTimeout;

	CompOption opt[ZOOM_SCREEN_OPTION_NUM];

	float speed;
	float step;
	float timestep;
	float maxScale;

	GLfloat currentScale;
	GLfloat newScale;
	GLfloat toScale;

	GLfloat zVelocity;

	GLfloat xscale;
	GLfloat yscale;

	Bool active;

	CursorTexture cursor;
	int mouseX;
	int mouseY;
	Bool cursorHidden;
	Bool cursorInfoSelected;
	Bool showScaled;
	Bool hideNormal;
	Bool noTimerUpdate;

	float maxTranslate;
} ZoomScreen;

#define GET_ZOOM_DISPLAY(d)                      \
    ((ZoomDisplay *) (d)->privates[displayPrivateIndex].ptr)

#define ZOOM_DISPLAY(d)                   \
    ZoomDisplay *zd = GET_ZOOM_DISPLAY (d)

#define GET_ZOOM_SCREEN(s, zd)                         \
    ((ZoomScreen *) (s)->privates[(zd)->screenPrivateIndex].ptr)

#define ZOOM_SCREEN(s)                                \
    ZoomScreen *zs = GET_ZOOM_SCREEN (s, GET_ZOOM_DISPLAY (s->display))

#define NUM_OPTIONS(s) (sizeof ((s)->opt) / sizeof (CompOption))

static void updateCursor(Display * dpy, CursorTexture * cursor)
{
	glEnable(GL_TEXTURE_RECTANGLE_ARB);
	if (!cursor->isSet)
	{
		cursor->isSet = TRUE;
		glGenTextures(1, &cursor->texture);
		glBindTexture(GL_TEXTURE_RECTANGLE_ARB, cursor->texture);
		glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
						GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
						GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
						GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
						GL_TEXTURE_WRAP_T, GL_CLAMP);
	}

	XFixesCursorImage *ci = XFixesGetCursorImage(dpy);

	cursor->width = ci->width;
	cursor->height = ci->height;
	cursor->hotX = ci->xhot;
	cursor->hotY = ci->yhot;

	unsigned char *pixels = malloc(ci->width * ci->height * 4);
	int i;

	for (i = 0; i < ci->width * ci->height; i++)
	{
		unsigned long pix = ci->pixels[i];

		pixels[i * 4] = pix & 0xff;
		pixels[(i * 4) + 1] = (pix >> 8) & 0xff;
		pixels[(i * 4) + 2] = (pix >> 16) & 0xff;
		pixels[(i * 4) + 3] = (pix >> 24) & 0xff;
	}

	glBindTexture(GL_TEXTURE_RECTANGLE_ARB, cursor->texture);
	glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGBA, cursor->width,
				 cursor->height, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
	glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
	glDisable(GL_TEXTURE_RECTANGLE_ARB);
	XFree(ci);
	free(pixels);
}

static void freeCursor(CursorTexture * cursor)
{
	if (!cursor->isSet)
		return;
	cursor->isSet = FALSE;
	glDeleteTextures(1, &cursor->texture);
	cursor->texture = 0;
}

static void zoomHandleEvent(CompDisplay * d, XEvent * event)
{
	CompScreen *s;

	ZOOM_DISPLAY(d);

	if (event->type == zd->fixesEventBase + XFixesCursorNotify)
	{
		XFixesCursorNotifyEvent *cev = (XFixesCursorNotifyEvent *) event;

		s = findScreenAtDisplay(d, cev->window);
		if (s)
		{
			ZOOM_SCREEN(s);
			if (zs->cursor.isSet)
				updateCursor(d->display, &zs->cursor);
		}
	}

	UNWRAP(zd, d, handleEvent);
	(*d->handleEvent) (d, event);
	WRAP(zd, d, handleEvent, zoomHandleEvent);
}

static Bool
zoomSetScreenOption(CompScreen * screen, char *name, CompOptionValue * value)
{
	CompOption *o;
	int index;

	ZOOM_SCREEN(screen);

	o = compFindOption(zs->opt, NUM_OPTIONS(zs), name, &index);
	if (!o)
		return FALSE;

	switch (index)
	{
	case ZOOM_SCREEN_OPTION_STEP:
		if (compSetFloatOption(o, value))
		{
			zs->step = o->value.f;
			return TRUE;
		}
		break;
	case ZOOM_SCREEN_OPTION_MAX_FACTOR:
		if (compSetFloatOption(o, value))
		{
			zs->maxScale = o->value.f;
			return TRUE;
		}
		break;
	case ZOOM_SCREEN_OPTION_SPEED:
		if (compSetFloatOption(o, value))
		{
			zs->speed = o->value.f;
			return TRUE;
		}
		break;
	case ZOOM_SCREEN_OPTION_TIMESTEP:
		if (compSetFloatOption(o, value))
		{
			zs->timestep = o->value.f;
			return TRUE;
		}
		break;
	case ZOOM_SCREEN_OPTION_FILTER_LINEAR:
		if (compSetBoolOption(o, value))
			return TRUE;
/*	case ZOOM_SCREEN_OPTION_HIDE_NORMAL_CURSOR:
		if (compSetBoolOption(o, value))
		{
			zs->hideNormal = o->value.b;
			return TRUE;
		}
		break;
	case ZOOM_SCREEN_OPTION_SHOW_SCALED_CURSOR:
		if (compSetBoolOption(o, value))
		{
			zs->showScaled = o->value.b;
			return TRUE;
		}
		break;*/
	case ZOOM_SCREEN_OPTION_MOUSE_UPDATE:
		if (compSetIntOption(o, value))
		{
			return TRUE;
		}
		break;
	case ZOOM_SCREEN_OPTION_OUT_ON_CUBE:
		if (compSetBoolOption(o, value))
		{
			return TRUE;
		}
		break;
	default:
		break;
	}

	return FALSE;
}

static void zoomScreenInitOptions(ZoomScreen * zs)
{
	CompOption *o;

	o = &zs->opt[ZOOM_SCREEN_OPTION_SPEED];
	o->advanced = False;
	o->name = "speed";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Zoom Speed");
	o->longDesc = N_("Zoom Speed.");
	o->type = CompOptionTypeFloat;
	o->value.f = ZOOM_SPEED_DEFAULT;
	o->rest.f.min = ZOOM_SPEED_MIN;
	o->rest.f.max = ZOOM_SPEED_MAX;
	o->rest.f.precision = ZOOM_SPEED_PRECISION;

	o = &zs->opt[ZOOM_SCREEN_OPTION_STEP];
	o->advanced = False;
	o->name = "step";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Step");
	o->longDesc =
			N_
			("Zoomfactor multiplier for amount of Zooming in each ZoomIn/ZoomOut.");
	o->type = CompOptionTypeFloat;
	o->value.f = ZOOM_STEP_DEFAULT;
	o->rest.f.min = ZOOM_STEP_MIN;
	o->rest.f.max = ZOOM_STEP_MAX;
	o->rest.f.precision = ZOOM_STEP_PRECISION;

	o = &zs->opt[ZOOM_SCREEN_OPTION_MAX_FACTOR];
	o->advanced = False;
	o->name = "max";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Maximum Zoom");
	o->longDesc = N_("Maximum Zoom factor.");
	o->type = CompOptionTypeFloat;
	o->value.f = ZOOM_MAX_FACTOR_DEFAULT;
	o->rest.f.min = ZOOM_MAX_FACTOR_MIN;
	o->rest.f.max = ZOOM_MAX_FACTOR_MAX;
	o->rest.f.precision = ZOOM_MAX_FACTOR_PRECISION;

	o = &zs->opt[ZOOM_SCREEN_OPTION_TIMESTEP];
	o->advanced = False;
	o->name = "timestep";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Zoom Timestep");
	o->longDesc = N_("Zoom Timestep.");
	o->type = CompOptionTypeFloat;
	o->value.f = ZOOM_TIMESTEP_DEFAULT;
	o->rest.f.min = ZOOM_TIMESTEP_MIN;
	o->rest.f.max = ZOOM_TIMESTEP_MAX;
	o->rest.f.precision = ZOOM_TIMESTEP_PRECISION;

	o = &zs->opt[ZOOM_SCREEN_OPTION_FILTER_LINEAR];
	o->advanced = False;
	o->name = "filter_linear";
	o->group = N_("Misc. options");
	o->subGroup = N_("Visual quality");
	o->displayHints = "";
	o->shortDesc = N_("Linear Filtering");
	o->longDesc = N_("Use Linear Filter when Zoomed in.");
	o->type = CompOptionTypeBool;
	o->value.b = ZOOM_FILTER_LINEAR_DEFAULT;

/*	o = &zs->opt[ZOOM_SCREEN_OPTION_HIDE_NORMAL_CURSOR];
	o->advanced = False;
	o->name = "hide_normal";
	o->group = N_("Misc. options");
	o->subGroup = N_("Cursor options");
	o->displayHints = "";
	o->shortDesc = N_("Hide Normal Cursor (May cause\nissues when switching window managers).");
	o->longDesc = N_("Hide Normal Cursor during Zoom.");
	o->type = CompOptionTypeBool;
	o->value.b = ZOOM_HIDE_NORMAL_CURSOR_DEFAULT;

	o = &zs->opt[ZOOM_SCREEN_OPTION_SHOW_SCALED_CURSOR];
	o->advanced = False;
	o->name = "show_scaled";
	o->group = N_("Misc. options");
	o->subGroup = N_("Cursor options");
	o->displayHints = "";
	o->shortDesc = N_("Show Scaled Cursor");
	o->longDesc = N_("Show Scaled Cursor during Zoom.");
	o->type = CompOptionTypeBool;
	o->value.b = ZOOM_SHOW_SCALED_CURSOR_DEFAULT;*/

	o = &zs->opt[ZOOM_SCREEN_OPTION_MOUSE_UPDATE];
	o->advanced = True;
	o->name = "mouse_update";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Mouse Update Interval");
	o->longDesc = N_("Mouse position Update Interval.");
	o->type = CompOptionTypeInt;
	o->value.i = ZOOM_MOUSE_UPDATE_DEFAULT;
	o->rest.i.min = ZOOM_MOUSE_UPDATE_MIN;
	o->rest.i.max = ZOOM_MOUSE_UPDATE_MAX;

	o = &zs->opt[ZOOM_SCREEN_OPTION_OUT_ON_CUBE];
	o->advanced = False;
	o->name = "zoomout_on_cube";
	o->group = N_("Misc. options");
	o->subGroup = N_("");
	o->displayHints = "";
	o->shortDesc = N_("Zoom Out on Cube");
	o->longDesc = N_("Zoom Out if the Cube gets activated.");
	o->type = CompOptionTypeBool;
	o->value.b = ZOOM_OUT_ON_CUBE_DEFAULT;
}
static CompOption *zoomGetScreenOptions(CompScreen * screen, int *count)
{
	if (screen)
	{
		ZOOM_SCREEN(screen);

		*count = NUM_OPTIONS(zs);
		return zs->opt;
	}
	else
	{
		ZoomScreen *zs = malloc(sizeof(ZoomScreen));

		zoomScreenInitOptions(zs);
		*count = NUM_OPTIONS(zs);
		return zs->opt;
	}
}


static int adjustZoomVelocity(ZoomScreen * zs)
{
	float d, adjust, amount;

	d = (zs->toScale - zs->currentScale) * 75.0f;

	adjust = d * 0.002f;
	amount = fabs(d);
	if (amount < 1.0f)
		amount = 1.0f;
	else if (amount > 5.0f)
		amount = 5.0f;

	zs->zVelocity = (amount * zs->zVelocity + adjust) / (amount + 1.0f);

	return (fabs(d) < 0.1f && fabs(zs->zVelocity) < 0.005f);
}

static void
zoomApplyScreenTransform(CompScreen * s,
						 const ScreenPaintAttrib * sAttrib, int output)
{
	ZOOM_SCREEN(s);

	UNWRAP(zs, s, applyScreenTransform);
	(*s->applyScreenTransform) (s, sAttrib, output);
	WRAP(zs, s, applyScreenTransform, zoomApplyScreenTransform);

	Bool *pCaps = (Bool *) IPCS_GetVPtrND(IPCS_OBJECT(s),
										  "CUBE_PAINTING_CAPS_BOOL_PTR",
										  NULL);

	if (zs->active && (!pCaps || !(*pCaps)))
	{
		float ox, oy, ow, oh;

		int mmMode = IPCS_GetIntND(IPCS_OBJECT(s), "MM_MODE", 0);

		if (mmMode == 1)
		{
			ox = 0;
			oy = 0;
			ow = s->width;
			oh = s->height;
		}
		else
		{
			ox = s->outputDev[output].region.extents.x1;
			oy = s->outputDev[output].region.extents.y1;
			ow = s->outputDev[output].width;
			oh = s->outputDev[output].height;
		}

		float tx = zs->mouseX - (ox + (ow / 2.0));
		float ty = zs->mouseY - (oy + (oh / 2.0));

		tx /= ow;
		tx = -(tx * (zs->xscale - 1));
		ty /= oh;
		ty = ty * (zs->yscale - 1);

		glTranslatef(tx, ty, 0.0f);
		glScalef(zs->xscale, zs->yscale, 1.0f);
	}
}

static Bool zoomUpdateMouse(void *vs)
{
	CompScreen *s = (CompScreen *) vs;

	ZOOM_SCREEN(s);

	if (zs->active &&
		!otherScreenGrabExist(s, "scale", "switcher", "move", "resize", 0))
	{
		int winX, winY;
		int rootX, rootY;
		unsigned int mask_return;
		Window root_return;
		Window child_return;

		XQueryPointer(s->display->display, s->root,
					  &root_return, &child_return,
					  &rootX, &rootY, &winX, &winY, &mask_return);

		if (rootX != zs->mouseX || rootY != zs->mouseY)
		{
			zs->mouseX = rootX;
			zs->mouseY = rootY;
			damageScreen(s);
		}
	}

	if (!zs->noTimerUpdate)
		zs->mouseTimeout =
			compAddTimeout(zs->opt[ZOOM_SCREEN_OPTION_MOUSE_UPDATE].value.i,
						   zoomUpdateMouse, s);
	return FALSE;
}

static void zoomPreparePaintScreen(CompScreen * s, int msSinceLastPaint)
{
	ZOOM_SCREEN(s);
	ZOOM_DISPLAY(s->display);

	if (screenGrabExist(s, "rotate", "cube", 0) &&
		zs->opt[ZOOM_SCREEN_OPTION_OUT_ON_CUBE].value.b)
		zs->toScale = 1.0f;
	else
		zs->toScale = zs->newScale;

	if (zs->active)
	{
		int steps;
		float amount, chunk;

		amount = msSinceLastPaint * 0.05f * zs->speed;
		steps = amount / (0.5f * zs->timestep);
		if (!steps)
			steps = 1;
		chunk = amount / (float)steps;

		while (steps--)
		{

			if (adjustZoomVelocity(zs))
			{
				zs->currentScale = zs->toScale;
				zs->zVelocity = 0.0f;
			}
			else
			{
				zs->currentScale +=
						(zs->zVelocity * msSinceLastPaint) / s->redrawTime;
			}

			zs->xscale = zs->currentScale;
			zs->yscale = zs->currentScale;

			if (zs->currentScale == 1.0f && zs->zVelocity == 0.0f &&
				zs->newScale == 1.0)
			{
				zs->active = FALSE;
				if (zs->mouseTimeout)
				{
					compRemoveTimeout(zs->mouseTimeout);
					zs->mouseTimeout = 0;
				}
				break;
			}

		}

		if (zs->currentScale == 1.0f && zs->zVelocity == 0.0f &&
			zs->newScale == 1.0)
		{
			zs->active = FALSE;
			if (zs->mouseTimeout)
			{
				compRemoveTimeout(zs->mouseTimeout);
				zs->mouseTimeout = 0;
			}
		}
	}

	if (!zs->active && zd->fixesSupported && zs->cursorInfoSelected)
	{
		zs->cursorInfoSelected = FALSE;
		XFixesSelectCursorInput(s->display->display, s->root, 0);
	}
	if (!zs->active && zs->cursor.isSet)
	{
		freeCursor(&zs->cursor);
	}
	if (zd->canHideCursor && !zs->active && zs->cursorHidden)
	{
		zs->cursorHidden = FALSE;
		XFixesShowCursor(s->display->display, s->root);
	}

	UNWRAP(zs, s, preparePaintScreen);
	(*s->preparePaintScreen) (s, msSinceLastPaint);
	WRAP(zs, s, preparePaintScreen, zoomPreparePaintScreen);
}

static void zoomDonePaintScreen(CompScreen * s)
{
	ZOOM_SCREEN(s);

	if (zs->active && zs->zVelocity != 0.0f)
	{
		damageScreen(s);
	}
	zs->noTimerUpdate = TRUE;
	zoomUpdateMouse(s);
	zs->noTimerUpdate = FALSE;

	UNWRAP(zs, s, donePaintScreen);
	(*s->donePaintScreen) (s);
	WRAP(zs, s, donePaintScreen, zoomDonePaintScreen);
}

static Bool
zoomPaintScreen(CompScreen * s,
				const ScreenPaintAttrib * sAttrib,
				Region region, int output, unsigned int mask)
{
	Bool status;

	ZOOM_SCREEN(s);

	if (zs->active)
	{
		mask &= ~PAINT_SCREEN_REGION_MASK;
		mask |= PAINT_SCREEN_TRANSFORMED_MASK | PAINT_SCREEN_CLEAR_MASK;

		int saveFilter = s->filter[SCREEN_TRANS_FILTER];

		if (zs->opt[ZOOM_SCREEN_OPTION_FILTER_LINEAR].value.b ||
			zs->zVelocity != 0.0f)
			s->filter[SCREEN_TRANS_FILTER] = COMP_TEXTURE_FILTER_GOOD;
		else
			s->filter[SCREEN_TRANS_FILTER] = COMP_TEXTURE_FILTER_FAST;

		UNWRAP(zs, s, paintScreen);
		status = (*s->paintScreen) (s, sAttrib, region, output, mask);
		WRAP(zs, s, paintScreen, zoomPaintScreen);

		s->filter[SCREEN_TRANS_FILTER] = saveFilter;
	}
	else
	{
		UNWRAP(zs, s, paintScreen);
		status = (*s->paintScreen) (s, sAttrib, region, output, mask);
		WRAP(zs, s, paintScreen, zoomPaintScreen);
	}

	return status;
}

static void
zoomPaintTransformedScreen(CompScreen * s, const ScreenPaintAttrib * sa,
						   Region region, int output, unsigned int mask)
{
	ZOOM_SCREEN(s);
	if (zs->active)
	{
		REGION r;

		r.rects = &r.extents;
		r.numRects = 1;

		if (screenGrabExist(s, "rotate", "cube", 0) &&
			IPCS_GetIntND(IPCS_OBJECT(s), "MM_MODE", 0) == 1)
		{
			r.extents.x1 = zs->mouseX - (zs->mouseX / zs->xscale);
			r.extents.y1 = zs->mouseY - (zs->mouseY / zs->yscale);
			r.extents.x2 = r.extents.x1 + ceil(s->width / zs->xscale) + 1;
			r.extents.y2 = r.extents.y1 + ceil(s->height / zs->yscale) + 1;
		}
		else
		{
			r.extents.x1 = zs->mouseX - (zs->mouseX / zs->xscale) +
					(s->outputDev[output].region.extents.x1 / zs->xscale);
			r.extents.y1 = zs->mouseY - (zs->mouseY / zs->yscale) +
					(s->outputDev[output].region.extents.y1 / zs->yscale);
			r.extents.x2 = r.extents.x1 +
					ceil(s->outputDev[output].width / zs->xscale) + 1;
			r.extents.y2 = r.extents.y1 +
					ceil(s->outputDev[output].height / zs->yscale) + 1;
		}


		r.extents.x1 = MAX(0, r.extents.x1);
		r.extents.y1 = MAX(0, r.extents.y1);
		r.extents.x2 = MIN(s->width, r.extents.x2);
		r.extents.y2 = MIN(s->height, r.extents.y2);

		UNWRAP(zs, s, paintTransformedScreen);
		(*s->paintTransformedScreen) (s, sa, &r, output, mask);
		WRAP(zs, s, paintTransformedScreen, zoomPaintTransformedScreen);

		if (zs->cursor.isSet)
		{
			glPushMatrix();
			glLoadIdentity();
			prepareXCoords(s, output, -DEFAULT_Z_CAMERA);

			glTranslatef(zs->mouseX, zs->mouseY, 0.0);
			glScalef(zs->xscale, zs->yscale, 1.0f);
			int x = -zs->cursor.hotX;
			int y = -zs->cursor.hotY;

			glEnable(GL_BLEND);
			glBindTexture(GL_TEXTURE_RECTANGLE_ARB, zs->cursor.texture);
			glEnable(GL_TEXTURE_RECTANGLE_ARB);

			glBegin(GL_QUADS);
			glTexCoord2d(0, 0);
			glVertex2f(x, y);
			glTexCoord2d(0, zs->cursor.height);
			glVertex2f(x, y + zs->cursor.height);
			glTexCoord2d(zs->cursor.width, zs->cursor.height);
			glVertex2f(x + zs->cursor.width, y + zs->cursor.height);
			glTexCoord2d(zs->cursor.width, 0);
			glVertex2f(x + zs->cursor.width, y);
			glEnd();

			glDisable(GL_BLEND);

			glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
			glDisable(GL_TEXTURE_RECTANGLE_ARB);
			glPopMatrix();
		}
	}
	else
	{
		UNWRAP(zs, s, paintTransformedScreen);
		(*s->paintTransformedScreen) (s, sa, region, output, mask);
		WRAP(zs, s, paintTransformedScreen, zoomPaintTransformedScreen);
	}
}

static Bool
zoomIn(CompDisplay * d,
	   CompAction * action,
	   CompActionState state, CompOption * option, int nOption)
{
	CompScreen *s;
	Window xid;

	ZOOM_DISPLAY(d);

	xid = getIntOptionNamed(option, nOption, "root", 0);

	s = findScreenAtDisplay(d, xid);
	if (s)
	{
		ZOOM_SCREEN(s);

		zs->active = TRUE;

		if (!zs->mouseTimeout)
			zs->mouseTimeout =
					compAddTimeout(zs->opt[ZOOM_SCREEN_OPTION_MOUSE_UPDATE].
								   value.i, zoomUpdateMouse, s);

		if (zd->fixesSupported && !zs->cursorInfoSelected && zs->showScaled)
		{
			zs->cursorInfoSelected = TRUE;
			XFixesSelectCursorInput(s->display->display,
									s->root, XFixesDisplayCursorNotifyMask);
			updateCursor(d->display, &zs->cursor);
		}
		if (zd->canHideCursor && !zs->cursorHidden && zs->hideNormal)
		{
			zs->cursorHidden = TRUE;
			XFixesHideCursor(d->display, s->root);
		}

		zs->newScale *= zs->step;
		if (zs->newScale > zs->maxScale)
			zs->newScale = zs->maxScale;

		damageScreen(s);
	}

	return TRUE;
}

static Bool
zoomOut(CompDisplay * d,
		CompAction * action,
		CompActionState state, CompOption * option, int nOption)
{
	CompScreen *s;
	Window xid;

	ZOOM_DISPLAY(d);

	xid = getIntOptionNamed(option, nOption, "root", 0);

	s = findScreenAtDisplay(d, xid);
	if (s)
	{
		ZOOM_SCREEN(s);

		zs->active = TRUE;

		if (!zs->mouseTimeout)
			zs->mouseTimeout =
					compAddTimeout(zs->opt[ZOOM_SCREEN_OPTION_MOUSE_UPDATE].
								   value.i, zoomUpdateMouse, s);

		if (zd->fixesSupported && !zs->cursorInfoSelected && zs->showScaled)
		{
			zs->cursorInfoSelected = TRUE;
			XFixesSelectCursorInput(s->display->display,
									s->root, XFixesDisplayCursorNotifyMask);
			updateCursor(d->display, &zs->cursor);
		}
		if (zd->canHideCursor && !zs->cursorHidden && zs->hideNormal)
		{
			zs->cursorHidden = TRUE;
			XFixesHideCursor(d->display, s->root);
		}

		zs->newScale /= zs->step;
		if (zs->newScale < 1.0)
			zs->newScale = 1.0;

		damageScreen(s);
	}

	return TRUE;
}

static void zoomUpdateCubeOptions(CompScreen * s)
{
	CompPlugin *p;

	ZOOM_SCREEN(s);

	p = findActivePlugin("cube");
	if (p && p->vTable->getScreenOptions)
	{
		CompOption *options, *option;
		int nOptions;

		options = (*p->vTable->getScreenOptions) (s, &nOptions);
		option = compFindOption(options, nOptions, "in", 0);
		if (option)
			zs->maxTranslate = option->value.b ? 0.85f : 1.5f;
	}
	else
	{
		zs->maxTranslate = 1.5f;
	}
}

static Bool
zoomSetScreenOptionForPlugin(CompScreen * s,
							 char *plugin,
							 char *name, CompOptionValue * value)
{
	Bool status;

	ZOOM_SCREEN(s);

	UNWRAP(zs, s, setScreenOptionForPlugin);
	status = (*s->setScreenOptionForPlugin) (s, plugin, name, value);
	WRAP(zs, s, setScreenOptionForPlugin, zoomSetScreenOptionForPlugin);

	if (status && strcmp(plugin, "cube") == 0)
		zoomUpdateCubeOptions(s);

	return status;
}

static Bool
zoomSetDisplayOption(CompDisplay * display,
					 char *name, CompOptionValue * value)
{
	CompOption *o;
	int index;

	ZOOM_DISPLAY(display);

	o = compFindOption(zd->opt, NUM_OPTIONS(zd), name, &index);
	if (!o)
		return FALSE;

	switch (index)
	{
	case ZOOM_DISPLAY_OPTION_IN:
	case ZOOM_DISPLAY_OPTION_OUT:
		if (setDisplayAction(display, o, value))
			return TRUE;
	default:
		break;
	}

	return FALSE;
}

static void zoomDisplayInitOptions(ZoomDisplay * zd)
{
	CompOption *o;

	o = &zd->opt[ZOOM_DISPLAY_OPTION_IN];
	o->advanced = False;
	o->name = "zoom_in";
	o->group = N_("Bindings");
	o->subGroup = N_("Zoom In");
	o->displayHints = "";
	o->shortDesc = N_("Zoom In");
	o->longDesc = N_("Zoom In.");
	o->type = CompOptionTypeAction;
	o->value.action.initiate = zoomIn;
	o->value.action.terminate = 0;
	o->value.action.bell = FALSE;
	o->value.action.edgeMask = 0;
	o->value.action.state = CompActionStateInitKey;
	o->value.action.state |= CompActionStateInitButton;
	o->value.action.type = CompBindingTypeButton;
	o->value.action.button.modifiers = ZOOM_IN_MODIFIERS_DEFAULT;
	o->value.action.button.button = ZOOM_IN_BUTTON_DEFAULT;

	o = &zd->opt[ZOOM_DISPLAY_OPTION_OUT];
	o->advanced = False;
	o->name = "zoom_out";
	o->group = N_("Bindings");
	o->subGroup = N_("Zoom Out");
	o->displayHints = "";
	o->shortDesc = N_("Zoom Out");
	o->longDesc = N_("Zoom Out.");
	o->type = CompOptionTypeAction;
	o->value.action.initiate = zoomOut;
	o->value.action.terminate = 0;
	o->value.action.bell = FALSE;
	o->value.action.edgeMask = 0;
	o->value.action.state = CompActionStateInitKey;
	o->value.action.state |= CompActionStateInitButton;
	o->value.action.type = CompBindingTypeButton;
	o->value.action.button.modifiers = ZOOM_OUT_MODIFIERS_DEFAULT;
	o->value.action.button.button = ZOOM_OUT_BUTTON_DEFAULT;
}

static CompOption *zoomGetDisplayOptions(CompDisplay * display, int *count)
{
	if (display)
	{
		ZOOM_DISPLAY(display);

		*count = NUM_OPTIONS(zd);
		return zd->opt;
	}
	else
	{
		ZoomDisplay *zd = malloc(sizeof(ZoomDisplay));

		zoomDisplayInitOptions(zd);
		*count = NUM_OPTIONS(zd);
		return zd->opt;
	}
}

static Bool zoomInitDisplay(CompPlugin * p, CompDisplay * d)
{
	ZoomDisplay *zd;

	zd = malloc(sizeof(ZoomDisplay));
	if (!zd)
		return FALSE;

	zd->screenPrivateIndex = allocateScreenPrivateIndex(d);
	if (zd->screenPrivateIndex < 0)
	{
		free(zd);
		return FALSE;
	}

	zoomDisplayInitOptions(zd);

	WRAP(zd, d, handleEvent, zoomHandleEvent);

	d->privates[displayPrivateIndex].ptr = zd;


	zd->fixesSupported =
			XFixesQueryExtension(d->display, &zd->fixesEventBase,
								 &zd->fixesErrorBase);

	int minor, major;

	XFixesQueryVersion(d->display, &major, &minor);

	if (major >= 4)
		zd->canHideCursor = TRUE;
	else
		zd->canHideCursor = FALSE;

	return TRUE;
}

static void zoomFiniDisplay(CompPlugin * p, CompDisplay * d)
{
	ZOOM_DISPLAY(d);

	UNWRAP(zd, d, handleEvent);
	freeScreenPrivateIndex(d, zd->screenPrivateIndex);

	free(zd);
}

static Bool zoomInitScreen(CompPlugin * p, CompScreen * s)
{
	ZoomScreen *zs;

	ZOOM_DISPLAY(s->display);

	zs = malloc(sizeof(ZoomScreen));
	if (!zs)
		return FALSE;

	zs->active = FALSE;

	zs->mouseTimeout = 0;

	zs->currentScale = 1.0f;
	zs->newScale = 1.0f;

	zs->zVelocity = 0.0f;

	zs->maxTranslate = 0.85f;

	zs->speed = ZOOM_SPEED_DEFAULT;
	zs->step = ZOOM_STEP_DEFAULT;
	zs->maxScale = ZOOM_MAX_FACTOR_DEFAULT;
	zs->timestep = ZOOM_TIMESTEP_DEFAULT;

	zoomScreenInitOptions(zs);

	addScreenAction(s, &zd->opt[ZOOM_DISPLAY_OPTION_IN].value.action);
	addScreenAction(s, &zd->opt[ZOOM_DISPLAY_OPTION_OUT].value.action);

	WRAP(zs, s, preparePaintScreen, zoomPreparePaintScreen);
	WRAP(zs, s, donePaintScreen, zoomDonePaintScreen);
	WRAP(zs, s, paintScreen, zoomPaintScreen);
	WRAP(zs, s, paintTransformedScreen, zoomPaintTransformedScreen);
	WRAP(zs, s, setScreenOptionForPlugin, zoomSetScreenOptionForPlugin);
	WRAP(zs, s, applyScreenTransform, zoomApplyScreenTransform);

	s->privates[zd->screenPrivateIndex].ptr = zs;

	zoomUpdateCubeOptions(s);

	zs->cursor.isSet = FALSE;
	zs->cursorHidden = FALSE;
	zs->cursorInfoSelected = FALSE;

	zs->showScaled = ZOOM_SHOW_SCALED_CURSOR_DEFAULT;
	zs->hideNormal = ZOOM_HIDE_NORMAL_CURSOR_DEFAULT;

	zs->noTimerUpdate = FALSE;

	return TRUE;
}

static void zoomFiniScreen(CompPlugin * p, CompScreen * s)
{
	ZOOM_SCREEN(s);
	ZOOM_DISPLAY(s->display);

	if (zs->mouseTimeout)
	{
		compRemoveTimeout(zs->mouseTimeout);
		zs->mouseTimeout = 0;
	}

	UNWRAP(zs, s, preparePaintScreen);
	UNWRAP(zs, s, donePaintScreen);
	UNWRAP(zs, s, paintScreen);
	UNWRAP(zs, s, paintTransformedScreen);
	UNWRAP(zs, s, setScreenOptionForPlugin);
	UNWRAP(zs, s, applyScreenTransform);

	removeScreenAction(s, &zd->opt[ZOOM_DISPLAY_OPTION_IN].value.action);
	removeScreenAction(s, &zd->opt[ZOOM_DISPLAY_OPTION_OUT].value.action);

	free(zs);
}


static Bool zoomInit(CompPlugin * p)
{
	displayPrivateIndex = allocateDisplayPrivateIndex();
	if (displayPrivateIndex < 0)
		return FALSE;

	return TRUE;
}

static void zoomFini(CompPlugin * p)
{
	if (displayPrivateIndex >= 0)
		freeDisplayPrivateIndex(displayPrivateIndex);
}

CompPluginDep zoomDeps[] = {
	{CompPluginRuleBefore, "cube"}
	,
};

CompPluginFeature zoomFeatures[] = {
	{"zoom"}
};

CompPluginVTable zoomVTable = {
	"inputzoom",
	N_("Input enabled Zoom"),
	N_("Input Enabled Zoom"),
	zoomInit,
	zoomFini,
	zoomInitDisplay,
	zoomFiniDisplay,
	zoomInitScreen,
	zoomFiniScreen,
	0,
	0,
	zoomGetDisplayOptions,
	zoomSetDisplayOption,
	zoomGetScreenOptions,
	zoomSetScreenOption,
	zoomDeps,
	sizeof(zoomDeps) / sizeof(zoomDeps[0]),
	zoomFeatures,
	sizeof(zoomFeatures) / sizeof(zoomFeatures[0]),
	BERYL_ABI_INFO,
	"beryl-plugins",
	"accessibility",
	0,
	0,
	True,
};

CompPluginVTable *getCompPluginInfo(void)
{
	return &zoomVTable;
}


syntax highlighted by Code2HTML, v. 0.9.1