/*
 * Copyright (c) 2001-2023 Julien Nadeau Carriere <vedge@csoft.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * The base container for Agar widgets. Agar windows are attached to some
 * parent AG_Driver(3), which forms a bridge between the Agar GUI system
 * and the user's selected graphics backend.
 */

#include <agar/core/core.h>

#include <agar/gui/gui.h>
#include <agar/gui/window.h>
#include <agar/gui/titlebar.h>
#include <agar/gui/icon.h>
#include <agar/gui/primitive.h>
#include <agar/gui/icons.h>
#include <agar/gui/cursors.h>
#include <agar/gui/label.h>
#if defined(AG_WIDGETS) && defined(AG_DEBUG)
#include <agar/gui/checkbox.h>
#include <agar/gui/scrollview.h>
#include <agar/gui/textbox.h>
#include <agar/gui/radio.h>
#include <agar/gui/numerical.h>
#include <agar/gui/separator.h>
#include <agar/gui/notebook.h>
#endif

#include <string.h>
#include <stdarg.h>

/* #define DEBUG_FOCUS */
/* #define DEBUG_REDRAW */
/* #define DEBUG_RAISE_LOWER */

int agWindowSideBorderDefault = 0;
int agWindowBotBorderDefault = 8;

/* Protected by agDrivers VFS lock */
AG_WindowQ agWindowDetachQ;             /* Async Window Detach queue */
AG_WindowQ agWindowShowQ;               /* Async Window Show queue */
AG_WindowQ agWindowHideQ;               /* Async Window Hide queue */
AG_Window *agWindowToFocus = NULL;      /* Window to focus next */
AG_Window *agWindowFocused = NULL;      /* Window holding focus */
Uint       agWindowPinnedCount = 0;     /* Number of pinned windows */

#if defined(AG_DEBUG) && defined(AG_WIDGETS)
AG_Window *_Nullable agDebuggerTgtWindow = NULL;     /* For GUI debugger */
#endif
#if defined(AG_WIDGETS)
AG_Window *_Nullable agStyleEditorTgtWindow = NULL;  /* For Style Editor */
#endif

#ifdef AG_WM_HINTS
/* Map enum ag_window_wm_type to EWMH window type */
const char *agWindowWmTypeNames[] = {
	"_NET_WM_WINDOW_TYPE_NORMAL",
	"_NET_WM_WINDOW_TYPE_DESKTOP",
	"_NET_WM_WINDOW_TYPE_DOCK",
	"_NET_WM_WINDOW_TYPE_TOOLBAR",
	"_NET_WM_WINDOW_TYPE_MENU",
	"_NET_WM_WINDOW_TYPE_UTILITY",
	"_NET_WM_WINDOW_TYPE_SPLASH",
	"_NET_WM_WINDOW_TYPE_DIALOG",
	"_NET_WM_WINDOW_TYPE_DROPDOWN_MENU",
	"_NET_WM_WINDOW_TYPE_POPUP_MENU",
	"_NET_WM_WINDOW_TYPE_TOOLTIP",
	"_NET_WM_WINDOW_TYPE_NOTIFICATION",
	"_NET_WM_WINDOW_TYPE_COMBO",
	"_NET_WM_WINDOW_TYPE_DND",
	NULL
};
#endif /* AG_WM_HINTS */

const char *agWindowAlignmentNames[] = {
	N_("None"),
	N_("Top Left"),		/* TL */
	N_("Top Center"),	/* TC */
	N_("Top Right"),	/* TR */
	N_("Middle Left"),	/* ML */
	N_("Middle Center"),	/* MC */
	N_("Middle Right"),	/* MR */
	N_("Bottom Left"),	/* BL */
	N_("Bottom Center"),	/* BC */
	N_("Bottom Right"),	/* BR */
	NULL
};

static int agWindowIconCounter = 0;

/*
 * Lookup a window by name.
 * The agDrivers VFS must be locked.
 */
AG_Window *
AG_WindowFind(const char *name)
{
	AG_Driver *drv;
	AG_Window *win;

	AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
		AG_FOREACH_WINDOW(win, drv) {
			AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
			if (strcmp(OBJECT(win)->name, name) == 0)
				return (win);
		}
	}
	return (NULL);
}

static __inline__ void
WindowNew_Common(AG_Window *win, Uint flags)
{
	static Uint winNo = 0;

	AG_ObjectInit(win, &agWindowClass);

	/* Name windows using a specific counter */
	AG_ObjectSetName(win, "win%u", winNo++);
	OBJECT(win)->flags &= ~(AG_OBJECT_NAME_ONATTACH);

	WIDGET(win)->window = win;
	win->flags |= flags;

	if (win->flags & AG_WINDOW_NORESIZE)
		win->flags |= AG_WINDOW_NOMAXIMIZE;              /* Implied */
	
	if ((win->flags & AG_WINDOW_FADEIN) ||
	    (win->flags & AG_WINDOW_FADEOUT)) {
		AG_WindowFadeCtx *fade;

		fade = win->pvt.fade = Malloc(sizeof(AG_WindowFadeCtx));
		fade->inTime = 0.06f;
		fade->inIncr = 0.2f;
		fade->outTime = 0.06f;
		fade->outIncr = 0.2f;
		fade->opacity = 1.0f;
		AG_InitTimer(&fade->timer, "fade", 0);
	}

	/*
	 * Set the default "close" action to AGWINDETACH(), a canned event
	 * handler which simply calls AG_ObjectDetach(win).
	 */
	AG_SetEvent(win, "window-close", AGWINDETACH(win));
}

/*
 * Create a new Agar window attached to an alternate driver instance.
 * Return a pointer to the newly-allocated window or NULL on failure.
 */
AG_Window *
AG_WindowNewUnder(void *pDrv, Uint flags)
{
	AG_Driver *drv = pDrv;
	AG_Window *win;

	if (!AG_OBJECT_VALID(drv) || !AG_DRIVER_ISA(drv)) {
		AG_SetErrorS("drv is not a Driver");
		return (NULL);
	}
	if ((win = TryMalloc(sizeof(AG_Window))) == NULL) {
		return (NULL);
	}
	WindowNew_Common(win, flags);

	AG_ObjectAttach(drv, win);
	
	if (!(win->flags & AG_WINDOW_NOTITLE)) {
		win->hMin += agTextFontHeight;
	}
	if (win->flags & AG_WINDOW_NOBORDERS) {
		win->wBorderSide = 0;
		win->wBorderBot = 0;
	}
	return (win);
}

/*
 * Create a new Agar window. On success return a pointer to the newly
 * allocated window. If not successful, fail and return NULL.
 */
AG_Window *
AG_WindowNew(Uint flags)
{
	AG_Driver *drv;
	AG_Window *win;

	if ((win = TryMalloc(sizeof(AG_Window))) == NULL) {
		return (NULL);
	}
	WindowNew_Common(win, flags);

	switch (agDriverOps->wm) {
	case AG_WM_SINGLE:
		AG_ObjectAttach(agDriverSw, win);
	
		if (!(win->flags & AG_WINDOW_NOTITLE)) {
			win->hMin += agTextFontHeight;
		}
		if (win->flags & AG_WINDOW_NOBORDERS) {
			win->wBorderSide = 0;
			win->wBorderBot = 0;
		}
		break;
	case AG_WM_MULTIPLE:
		if ((drv = AG_DriverOpen(agDriverOps, NULL)) == NULL) {
			Verbose("%s: Failed (%s)\n", agDriverOps->name, AG_GetError());
			return (NULL);
		}
		AG_ObjectAttach(drv, win);

		AGDRIVERMW(drv)->win = win;

		win->wBorderSide = 0;
		win->wBorderBot = 0;
		break;
	}
	return (win);
}

/*
 * Create a new, uniquely named Agar window (string name).
 * On success return a pointer to the new window.
 * If the named window exists, focus the existing window and return NULL.
 */
AG_Window *
AG_WindowNewNamedS(Uint flags, const char *name)
{
	AG_Window *win;

	AG_LockVFS(&agDrivers);

	if (AG_WindowFocusNamed(name)) {
		win = NULL;
		goto out;
	}
	if ((win = AG_WindowNew(flags)) == NULL) {
		AG_FatalError(NULL);
	}
	AG_ObjectSetNameS(win, name);
	AG_SetEvent(win, "window-close", AGWINHIDE(win));
out:
	AG_UnlockVFS(&agDrivers);
	return (win);
}

/*
 * Create a new, uniquely named Agar window (format string name).
 * On success return a pointer to the new window.
 * If the named window exists, focus the existing window and return NULL.
 */
AG_Window *
AG_WindowNewNamed(Uint flags, const char *fmt, ...)
{
	char s[AG_OBJECT_NAME_MAX];
	va_list ap;

	va_start(ap, fmt);
	Vsnprintf(s, sizeof(s), fmt, ap);
	va_end(ap);

	return AG_WindowNewNamedS(flags, s);
}

/* Special implementation of AG_ObjectAttach() for AG_Window. */
static void
Attach(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();
	AG_Driver *drv = OBJECT(win)->parent;

	AG_OBJECT_ISA(drv, "AG_Driver:*");

	/* Attach the window. */
	if (win->flags & AG_WINDOW_KEEPBELOW) {
		TAILQ_INSERT_HEAD(&OBJECT(drv)->children, OBJECT(win), cobjs);
	} else {
		TAILQ_INSERT_TAIL(&OBJECT(drv)->children, OBJECT(win), cobjs);
	}
	if (AGDRIVER_MULTIPLE(drv))
		AGDRIVERMW(drv)->win = win;

	/*
	 * Notify the objects. This will cause the the "drv" and "drvOps"
	 * pointers of all widgets to be updated.
	 */
	AG_PostEvent(win, "attached", "%p", drv);

#ifdef AG_WIDGETS
	if (AGDRIVER_SINGLE(drv)) {
		/*
		 * Initialize built-in titlebar and desktop icon now that we
		 * have an attached driver (we cannot do this earlier because
		 * AG_Icon(3) requires surface operations).
		 */
		AG_Icon *icon;

		if (win->tbar == NULL && (win->flags & AG_WINDOW_NOTITLE) == 0) {
			Uint tbFlags = 0;

			if (win->flags & AG_WINDOW_NOCLOSE)    { tbFlags |= AG_TITLEBAR_NO_CLOSE; }
			if (win->flags & AG_WINDOW_NOMINIMIZE) { tbFlags |= AG_TITLEBAR_NO_MINIMIZE; }
			if (win->flags & AG_WINDOW_NOMAXIMIZE) { tbFlags |= AG_TITLEBAR_NO_MAXIMIZE; }

			win->tbar = AG_TitlebarNew(win, tbFlags);
		}
		if ((icon = win->icon) == NULL) {
			icon = win->icon = AG_IconNew(NULL, 0);
			AG_ObjectSetNameS(icon, "icon");
		}

		WIDGET(icon)->drv = drv;
		WIDGET(icon)->drvOps = AGDRIVER_CLASS(drv);

		AG_IconSetSurface(icon, agIconWindow.s);
		AG_IconSetBackgroundFill(icon, 1, &AGDRIVERSW(drv)->bgColor);
		AG_SetFontSize(icon, "80%");
		AG_WidgetCompileStyle(icon);
	}
#endif /* AG_WIDGETS */

	if (win->flags & AG_WINDOW_FOCUSONATTACH)
		AG_WindowFocus(win);
}

/* Special implementation of AG_ObjectDetach() for AG_Window. */
static void
Detach(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();
	AG_Driver *drv;
	AG_Window *other, *subwin;
	AG_Timer *to, *toNext;

	AG_LockVFS(&agDrivers);

	/* Mark window detach in progress */
	win->flags |= AG_WINDOW_DETACHING;

	/* Cancel any running timer attached to the window. */
	AG_LockTiming();
	for (to = TAILQ_FIRST(&OBJECT(win)->timers);
	     to != TAILQ_END(&OBJECT(win)->timers);
	     to = toNext) {
		toNext = TAILQ_NEXT(to, pvt.timers);
		AG_DelTimer(win, to);
	}
	AG_UnlockTiming();

	/* Implicitely detach window dependencies. */
	AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
		AG_OBJECT_ISA(drv, "AG_Driver:*");

		AG_FOREACH_WINDOW(other, drv) {
			if (other == win) {
				continue;
			}
			AG_OBJECT_ISA(other, "AG_Widget:AG_Window:*");
			AG_ObjectLock(other);
			TAILQ_FOREACH(subwin, &other->pvt.subwins, pvt.swins) {
				if (subwin == win)
					break;
			}
			if (subwin) {
				TAILQ_REMOVE(&other->pvt.subwins, subwin, pvt.swins);
			}
			if (other->pinnedTo == win) {
				AG_WindowUnpin(other);
			}
			if (other->parent == win ||
			    other->transientFor == win) {
				AG_ObjectDetach(other);
			}
			AG_ObjectUnlock(other);
		}
	}

 	/*
	 * For the AG_ObjectDetach() call to be safe in (free-threaded) event
	 * context, we must defer the actual window hide / detach operation
	 * until the end of the current event processing cycle.
	 */
	TAILQ_INSERT_TAIL(&agWindowDetachQ, win, pvt.detach);

	/*
	 * Cull any redundant visibility ops against the newly detached window.
	 */
	TAILQ_FOREACH(other, &agWindowHideQ, pvt.visibility) {
		if (other == win) {
			TAILQ_REMOVE(&agWindowHideQ, win, pvt.visibility);
			break;
		}
	}
	TAILQ_FOREACH(other, &agWindowShowQ, pvt.visibility) {
		if (other == win) {
			TAILQ_REMOVE(&agWindowShowQ, win, pvt.visibility);
			break;
		}
	}
	
	AG_UnlockVFS(&agDrivers);
}

static Uint32
FadeTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();
	const int dir = AG_INT(1);
	AG_WindowFadeCtx *fade = win->pvt.fade;

	if (dir == 1) {					/* Fade in */
		if (fade->opacity < 1.0f) {
			fade->opacity += fade->inIncr;
			AG_WindowSetOpacity(win, fade->opacity);
			return (to->ival);
		} else {
			return (0);
		}
	} else {					/* Fade out */
		if (fade->opacity > 0.0f) {
			fade->opacity -= fade->outIncr;
			AG_WindowSetOpacity(win, fade->opacity);
			return (to->ival);
		} else {
			AG_WindowSetOpacity(win, 1.0f);

			/* Defer operation until AG_WindowProcessQueued(). */
			AG_LockVFS(&agDrivers);
			TAILQ_INSERT_TAIL(&agWindowHideQ, win, pvt.visibility);
			AG_UnlockVFS(&agDrivers);
			return (0);
		}
	}
}

/*
 * Make a window a logical child of the specified window. It will be detached
 * automatically when the parent is detached (independently of VFS layout).
 */
void
AG_WindowAttach(AG_Window *winParent, AG_Window *winChld)
{
	if (winParent == NULL)
		return;

	AG_OBJECT_ISA(winParent, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(winChld, "AG_Widget:AG_Window:*");

	AG_LockVFS(&agDrivers);
	AG_ObjectLock(winParent);
	AG_ObjectLock(winChld);

	if (winChld->parent) {
		if (winChld->parent == winParent) {
			goto out;
		}
		AG_WindowDetach(winChld->parent, winChld);
	}

	winChld->parent = winParent;
	winChld->zoom = winParent->zoom;

	AG_WidgetCopyStyle(winChld, winParent);
	TAILQ_INSERT_HEAD(&winParent->pvt.subwins, winChld, pvt.swins);
out:
	AG_ObjectUnlock(winChld);
	AG_ObjectUnlock(winParent);
	AG_UnlockVFS(&agDrivers);
}

/* Detach a window from its logical parent. */
void
AG_WindowDetach(AG_Window *winParent, AG_Window *winChld)
{
	if (winParent == NULL)
		return;

	AG_OBJECT_ISA(winParent, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(winChld, "AG_Widget:AG_Window:*");

	AG_LockVFS(&agDrivers);
	AG_ObjectLock(winParent);
	AG_ObjectLock(winChld);

#ifdef AG_DEBUG
	if (winChld->parent != winParent)
		AG_FatalError("Inconsistent AG_WindowDetach()");
#endif
	TAILQ_REMOVE(&winParent->pvt.subwins, winChld, pvt.swins);
	winChld->parent = NULL;

	AG_ObjectUnlock(winChld);
	AG_ObjectUnlock(winParent);
	AG_UnlockVFS(&agDrivers);
}

/*
 * Make a window a transient window for another window. The effect of
 * this setting is WM-dependent (see AG_Window(3) for details).
 */
void
AG_WindowMakeTransient(AG_Window *forParent, AG_Window *win)
{
	AG_Driver *drv = WIDGET(win)->drv;
	
	AG_LockVFS(&agDrivers);
	AG_ObjectLock(win);

	if (forParent == NULL) {
		if (drv && AGDRIVER_MULTIPLE(drv) &&
		    AGDRIVER_MW_CLASS(drv)->setTransientFor != NULL) {
			AGDRIVER_MW_CLASS(drv)->setTransientFor(win, NULL);
		}
		win->transientFor = NULL;
	} else {
		AG_ObjectLock(forParent);
		if (drv && AGDRIVER_MULTIPLE(drv) &&
		    AGDRIVER_MW_CLASS(drv)->setTransientFor != NULL) {
			AGDRIVER_MW_CLASS(drv)->setTransientFor(win, forParent);
		}
		win->transientFor = forParent;
		AG_ObjectUnlock(forParent);
	}

	AG_ObjectUnlock(win);
	AG_UnlockVFS(&agDrivers);
}

/*
 * Pin a window win against another window winParent such that win will
 * be moved along whenever winParent is moved.
 */
void
AG_WindowPin(AG_Window *winParent, AG_Window *win)
{
#ifdef AG_DEBUG
	if (win == winParent) { AG_FatalError("AG_WindowPin"); }
#endif
	AG_OBJECT_ISA(winParent, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	if (win->pinnedTo == NULL) {
		agWindowPinnedCount++;
	}
	win->pinnedTo = winParent;

	AG_ObjectUnlock(win);
}

/* Unpin a window. */
void
AG_WindowUnpin(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	if (win->pinnedTo != NULL) {
		win->pinnedTo = NULL;
		agWindowPinnedCount--;
	}

	AG_ObjectUnlock(win);
}

static void
MovePinnedRecursive(AG_Window *_Nonnull win, int xRel, int yRel)
{
	AG_Rect r;
	AG_Window *winOther;
	AG_Driver *drv;

	r.x = WIDGET(win)->x + xRel;
	r.y = WIDGET(win)->y + yRel;
	r.w = WIDGET(win)->w;
	r.h = WIDGET(win)->h;
	AG_WindowSetGeometryRect(win, &r, 0);
	
	AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
		AG_FOREACH_WINDOW(winOther, drv) {
			if (winOther->pinnedTo == win)
				MovePinnedRecursive(winOther, xRel,yRel);
		}
	}
}

/* Move pinned windows by a relative amount. Called by driver code. */
void
AG_WindowMovePinned(AG_Window *winParent, int xRel, int yRel)
{
	AG_Driver *drv;
	AG_Window *win;

	AG_LockVFS(&agDrivers);

	AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
		AG_FOREACH_WINDOW(win, drv) {
			if (win->pinnedTo == winParent)
				MovePinnedRecursive(win, xRel, yRel);
		}
	}

	AG_UnlockVFS(&agDrivers);
}

/*
 * Test whether a widget (or one of its children) is requesting a
 * geometry update.
 */
static int _Pure_Attribute
UpdateNeeded(AG_Widget *_Nonnull wid)
{
	AG_Widget *chld;

	if (wid->flags & AG_WIDGET_UPDATE_WINDOW) {
		return (1);
	}
	OBJECT_FOREACH_CHILD(chld, wid, ag_widget) {
		if (UpdateNeeded(chld))
			return (1);
	}
	return (0);
}

/*
 * Test whether a window is currently selected for a given WM operation.
 * The agDrivers VFS must be locked.
 */
static __inline__ int _Pure_Attribute
Selected_WM_Op(AG_Window *_Nonnull win, enum ag_wm_operation op)
{
	AG_Driver *drv = WIDGET(win)->drv;

	return (AGDRIVER_SINGLE(drv) &&
	        AGDRIVERSW(drv)->winSelected == win &&
	        AGDRIVERSW(drv)->winop == op);
}

#define DRAW_BORDER(winop,c)			\
	if (Selected_WM_Op(win, winop)) {	\
		AG_DrawBoxSunk(win, &r, c);	\
	} else {				\
		AG_DrawBoxRaised(win, &r, c);	\
	}

static void
Draw(void *_Nonnull obj)
{
	AG_Window *win = obj;
	const AG_Color *cFg = &WCOLOR(win,FG_COLOR);
	AG_Widget *chld;
	AG_Rect r;
	const int w = WIDTH(win);
	const int h = HEIGHT(win);
	const int hBar = (win->tbar) ? HEIGHT(win->tbar) : 0;
	const int wBorderBot = win->wBorderBot;
	int wBorderSide;

	if (UpdateNeeded(WIDGET(win)))
		AG_WindowUpdate(win);

	/* Render window background. */
	if ((win->flags & AG_WINDOW_NOBACKGROUND) == 0 &&
	    (WIDGET(win)->drv->flags & AG_DRIVER_WINDOW_BG) == 0) {
		r.x = 0;
		r.y = hBar-1;
		r.w = w;
		r.h = h-hBar;
		AG_DrawRect(win, &r, &WCOLOR(win,BG_COLOR));
	}

	/* Render decorative borders. */
	if (wBorderBot > 0) {
		const int wResizeCtrl = win->wResizeCtrl;
		const int wResizeCtrl2 = (wResizeCtrl << 1);

		/* XXX top overdraw */
		AG_DrawRectOutline(win, &WIDGET(win)->r, &WCOLOR(win,LINE_COLOR));

		r.x = 1;
		r.y = h - wBorderBot;
		r.h = wBorderBot;
		if ((win->flags & AG_WINDOW_NORESIZE) == 0 && w > wResizeCtrl2) {
			r.w = wResizeCtrl;
			DRAW_BORDER(AG_WINOP_LRESIZE, cFg);
			r.x = w - wResizeCtrl;
			DRAW_BORDER(AG_WINOP_RRESIZE, cFg);
			r.x = wResizeCtrl + 1;
			r.w = w - wResizeCtrl2 - 1;
			DRAW_BORDER(AG_WINOP_HRESIZE, cFg);
		} else {
			r.w = w;
			AG_DrawBoxRaised(win, &r, cFg);
		}
	}
	if ((wBorderSide = win->wBorderSide) > 0) {
		r.x = 0;
		r.y = hBar;
		r.w = wBorderSide;
		r.h = h - wBorderBot - hBar;
		AG_DrawBoxRaised(win, &r, cFg);
		r.x = w - wBorderSide;
		AG_DrawBoxRaised(win, &r, cFg);
	}

	OBJECT_FOREACH_CHILD(chld, win, ag_widget)
		AG_WidgetDraw(chld);
}

#undef DRAW_BORDER

static void
OnShow(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();
	AG_Driver *drv = WIDGET(win)->drv;
	AG_Object *chld;
	AG_SizeReq r;
	AG_SizeAlloc a;
	int xPref, yPref;
	Uint mwFlags = 0;
	
	AG_OBJECT_ISA(drv, "AG_Driver:*");

	win->visible = 1;
	WIDGET(win)->flags |= AG_WIDGET_VISIBLE;

	AG_WidgetCompileStyle(win);

	if (WIDGET(win)->x == -1 || /* Initial size requisition & allocation */
	    WIDGET(win)->y == -1) {
		AG_WidgetSizeReq(win, &r);

		if (AGDRIVER_SINGLE(drv)) {
			AG_WM_GetPrefPosition(win, &xPref, &yPref, r.w, r.h);
			a.x = xPref;
			a.y = yPref;
		} else {
			a.x = 0;
			a.y = 0;
		}
		a.w = r.w;
		a.h = r.h;

		if (win->alignment != AG_WINDOW_ALIGNMENT_NONE) {
			if (AGDRIVER_MULTIPLE(drv))
				AG_WindowComputeAlignment(win, &a);
		} else {
#ifdef AG_WM_HINTS
			if (AGDRIVER_MULTIPLE(drv) &&
			   (AGDRIVERMW(drv)->flags & AG_DRIVER_MW_ANYPOS_AVAIL)) {
				/* Let the WM choose a default position */
				mwFlags |= AG_DRIVER_MW_ANYPOS;
			} else
#endif
			{
				win->alignment = AG_WINDOW_MC;
				AG_WindowComputeAlignment(win, &a);
			}
		}
	} else {
		a.x = WIDGET(win)->x;
		a.y = WIDGET(win)->y;
		a.w = WIDTH(win);
		a.h = HEIGHT(win);
	}
	AG_WidgetSizeAlloc(win, &a);
	
	switch (AGDRIVER_CLASS(drv)->wm) {
	case AG_WM_SINGLE:
		AG_WidgetUpdateCoords(win,
		    WIDGET(win)->x,
		    WIDGET(win)->y);
		break;
	case AG_WM_MULTIPLE:
		if (!(AGDRIVERMW(drv)->flags & AG_DRIVER_MW_OPEN)) {
			AG_Rect rw;

			rw.x = a.x;
			rw.y = a.y;
			rw.w = a.w;
			rw.h = a.h;
			if (AGDRIVER_MW_CLASS(drv)->openWindow(win, &rw, 0,
			    mwFlags) == -1) {
				AG_FatalError(NULL);
			}
			AGDRIVERMW(drv)->flags |= AG_DRIVER_MW_OPEN;
		}
		if (win->flags & AG_WINDOW_FADEIN)
			AG_WindowSetOpacity(win, 0.0f);

		if (AGDRIVER_MW_CLASS(drv)->mapWindow(win) == -1)
			AG_FatalError(NULL);

		if ((win->flags & AG_WINDOW_UPDATECAPTION) &&
		    AGDRIVER_MW_CLASS(drv)->setWindowCaption != NULL) {
			win->flags &= ~(AG_WINDOW_UPDATECAPTION);
			AGDRIVER_MW_CLASS(drv)->setWindowCaption(win, win->caption);
		}

		AG_WidgetUpdateCoords(win, 0,0);
		break;
	}
	
	OBJECT_FOREACH_CHILD(chld, win, ag_object) {      /* Notify widgets */
		AG_ForwardEvent(chld, event);
	}
	AG_PostEvent(win, "window-shown", NULL);

	if (!(win->flags & AG_WINDOW_DENYFOCUS))   /* Set focus implicitely */
		agWindowToFocus = win;

	win->dirty = 1;                           /* Request display update */
	win->flags &= ~(AG_WINDOW_NOCURSORCHG);     /* Allow cursor changes */

	if (win->flags & AG_WINDOW_FADEIN) {
		AG_WindowFadeCtx *fade = win->pvt.fade;

		AG_AddTimer(win, &fade->timer,
		    (Uint32)((fade->inTime*1000.0) / (1.0 / fade->inIncr)),
		    FadeTimeout, "%i", 1);
	}
}

static void
OnHide(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();
	AG_Driver *drv = WIDGET(win)->drv;
	AG_DriverSw *dsw;
	AG_Object *chld;

	AG_OBJECT_ISA(drv, "AG_Driver:*");

	win->visible = 0;
	WIDGET(win)->flags &= ~(AG_WIDGET_VISIBLE);

	win->dirty = 0;                       /* Cancel any display updates */
	win->flags |= AG_WINDOW_NOCURSORCHG;     /* Disallow cursor changes */
	if (win == agWindowToFocus)            /* Cancel any focus requests */
		agWindowToFocus = NULL;

	switch (AGDRIVER_CLASS(drv)->wm) {
	case AG_WM_SINGLE:
		dsw = (AG_DriverSw *)drv;
#ifdef AG_DEBUG	
		if (OBJECT(drv)->parent == NULL) 
			AG_FatalError("NULL parent");
#endif
		if (win == agWindowFocused) {                 /* Move focus */
			AG_Window *wOther;

			AG_PostEvent(win, "window-lostfocus", NULL);
			agWindowFocused = NULL;
		
			/* Focus the next (highest) window on the stack. */
			AG_FOREACH_WINDOW_REVERSE(wOther, dsw) {
				if (!wOther->visible) {
					continue;
				}
				AG_ObjectLock(wOther);
				if ((wOther->flags & AG_WINDOW_DENYFOCUS) == 0) {
					AG_ObjectUnlock(wOther);
					break;
				}
				AG_ObjectUnlock(wOther);
			}
			if (wOther != NULL)
				agWindowToFocus = wOther;
		}

		if (AGDRIVER_CLASS(drv)->type == AG_FRAMEBUFFER) {
			AG_Rect rFill = WIDGET(win)->r;

			rFill.x--;
			rFill.y--;
			rFill.w+=2;
			rFill.h+=2;
			AG_DrawRectFilled(win, &rFill, &dsw->bgColor);

			if (AGDRIVER_CLASS(drv)->updateRegion != NULL) {
				AG_Rect r;

				r.x = WIDGET(win)->x;
				r.y = WIDGET(win)->y;
				r.w = WIDTH(win)  + 1;
				r.h = HEIGHT(win) + 1;
				AGDRIVER_CLASS(drv)->updateRegion(drv, &r);
			}
		}
		break;
	case AG_WM_MULTIPLE:
		if (win == agWindowFocused) {               /* Cancel focus */
			AG_PostEvent(win, "window-lostfocus", NULL);
			agWindowFocused = NULL;
		}
		if (AGDRIVERMW(drv)->flags & AG_DRIVER_MW_OPEN) {
			if (AGDRIVER_MW_CLASS(drv)->unmapWindow(win) == -1)
				AG_FatalError(NULL);
		}
		break;
	}
	
	OBJECT_FOREACH_CHILD(chld, win, ag_object) {     /* Notify children */
		AG_ForwardEvent(chld, event);
	}
	AG_PostEvent(win, "window-hidden", NULL);
}

static void
OnDetach(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();
	AG_Object *chld;

#if defined(AG_DEBUG) && defined(AG_WIDGETS)
	if (win == agDebuggerTgtWindow)
		AG_GuiDebuggerDetachWindow();
#endif
	/*
	 * Forward the "detached" event to child objects. The "detached"
	 * handler of the AG_Widget class is expected to forward the event
	 * to child widgets.
	 */
	OBJECT_FOREACH_CHILD(chld, win, ag_object) {
		event->argv[1].data.p = win;                      /* SENDER */
		AG_ForwardEvent(chld, event);
	}
}

static void
WidgetGainFocus(AG_Widget *_Nonnull wid)
{
	AG_Widget *chld;

	OBJECT_FOREACH_CHILD(chld, wid, ag_widget) {
		AG_OBJECT_ISA(chld, "AG_Widget:*");
		AG_ObjectLock(chld);
		WidgetGainFocus(chld);
		AG_ObjectUnlock(chld);
	}
	if (wid->flags & AG_WIDGET_FOCUSED)
		AG_PostEvent(wid, "widget-gainfocus", NULL);
}

static void
WidgetLostFocus(AG_Widget *_Nonnull wid)
{
	AG_Widget *chld;

	OBJECT_FOREACH_CHILD(chld, wid, ag_widget) {
		AG_OBJECT_ISA(chld, "AG_Widget:*");
		AG_ObjectLock(chld);
		WidgetLostFocus(chld);
		AG_ObjectUnlock(chld);
	}
	if (wid->flags & AG_WIDGET_FOCUSED)
		AG_PostEvent(wid, "widget-lostfocus", NULL);
}

static void
OnGainFocus(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();

#ifdef DEBUG_FOCUS
	Debug(win, "Focus Gained (\"%s\")\n", win->caption);
#endif
	WidgetGainFocus(WIDGET(win));
}

static void
OnLostFocus(AG_Event *_Nonnull event)
{
	AG_Window *win = AG_WINDOW_SELF();

#ifdef DEBUG_FOCUS
	Debug(win, "Focus Lost (\"%s\")\n", win->caption);
#endif
	WidgetLostFocus(WIDGET(win));
}

/* Lower a window to the bottom of the stack. */
void
AG_WindowLower(AG_Window *win)
{
	AG_Driver *drv = WIDGET(win)->drv;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(drv, "AG_Driver:*");
#ifdef DEBUG_RAISE_LOWER
	Debug(drv, "Lowering %s to bottom\n", OBJECT(win)->name);
#endif
	AG_LockVFS(&agDrivers);
	AG_ObjectLock(drv);
	AG_ObjectLock(win);

	switch (AGDRIVER_CLASS(drv)->wm) {
	case AG_WM_MULTIPLE:
		AGDRIVER_MW_CLASS(drv)->lowerWindow(win);
		break;
	case AG_WM_SINGLE:
		TAILQ_REMOVE(&OBJECT(drv)->children, OBJECT(win), cobjs);
		TAILQ_INSERT_HEAD(&OBJECT(drv)->children, OBJECT(win), cobjs);
		break;
	}

	AG_ObjectUnlock(drv);
	AG_ObjectUnlock(win);
	AG_UnlockVFS(&agDrivers);
}

/* Raise a window to the top of the stack. */
void
AG_WindowRaise(AG_Window *win)
{
	AG_Driver *drv = WIDGET(win)->drv;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(drv, "AG_Driver:*");
#ifdef DEBUG_RAISE_LOWER
	Debug(drv, "Raising %s to top\n", OBJECT(win)->name);
#endif
	AG_LockVFS(&agDrivers);
	AG_ObjectLock(drv);
	AG_ObjectLock(win);

	switch (AGDRIVER_CLASS(drv)->wm) {
	case AG_WM_MULTIPLE:
		AGDRIVER_MW_CLASS(drv)->raiseWindow(win);
		break;
	case AG_WM_SINGLE:
		TAILQ_REMOVE(&OBJECT(drv)->children, OBJECT(win), cobjs);
		TAILQ_INSERT_TAIL(&OBJECT(drv)->children, OBJECT(win), cobjs);
		break;
	}

	AG_ObjectUnlock(drv);
	AG_ObjectUnlock(win);
	AG_UnlockVFS(&agDrivers);
}

/* Make a window visible to the user. */
void
AG_WindowShow(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_LockVFS(&agDrivers);
	AG_ObjectLock(win);

	if (!win->visible) {
#ifdef AG_THREADS
		if (!AG_ThreadEqual(AG_ThreadSelf(), agEventThread)) {
			TAILQ_INSERT_TAIL(&agWindowShowQ, win, pvt.visibility);
		} else
#endif
		{
			AG_PostEvent(win, "widget-shown", NULL);
		}
	}

	AG_ObjectUnlock(win);
	AG_UnlockVFS(&agDrivers);
}

/* Make a window invisible to the user. */
void
AG_WindowHide(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_LockVFS(&agDrivers);
	AG_ObjectLock(win);

	if (!win->visible) {
		goto out;
	}
	if ((win->flags & AG_WINDOW_FADEOUT) &&
	   !(win->flags & AG_WINDOW_DETACHING)) {
		AG_WindowFadeCtx *fade = win->pvt.fade;

		AG_AddTimer(win, &fade->timer,
		    (Uint32)((fade->outTime * 1000.0) / (1.0 / fade->outIncr)),
		    FadeTimeout, "%i", -1);
	} else {
#ifdef AG_THREADS
		if (!AG_ThreadEqual(AG_ThreadSelf(), agEventThread)) {
			TAILQ_INSERT_TAIL(&agWindowHideQ, win, pvt.visibility);
		} else
#endif
		{
			AG_PostEvent(win, "widget-hidden", NULL);
			win->visible = 0;
			WIDGET(win)->flags &= ~(AG_WIDGET_VISIBLE);
		}
	}
out:
	AG_ObjectUnlock(win);
	AG_UnlockVFS(&agDrivers);
}

/*
 * Render all windows that need to be redrawn. This is typically invoked
 * by the main event loop after all events have been processed.
 */ 
void
AG_WindowDrawQueued(void)
{
	AG_Driver *drv;
	AG_Window *win;

	AG_LockVFS(&agDrivers);

	AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
		switch (AGDRIVER_CLASS(drv)->wm) {
		case AG_WM_MULTIPLE:
			if ((win = AGDRIVERMW(drv)->win) != NULL) {
				if (!win->visible || !win->dirty) {
					continue;
				}
				AG_ObjectLock(win);
				AG_BeginRendering(drv);
				AGDRIVER_CLASS(drv)->renderWindow(win);
				AG_EndRendering(drv);
				win->dirty = 0;
				AG_ObjectUnlock(win);
			}
			break;
		case AG_WM_SINGLE:
			{
				AG_DriverSw *dsw = (AG_DriverSw *)drv;
				Uint32 t;
				int doRedraw;

				t = AG_GetTicks();
				if ((t - dsw->rLast) < dsw->rNom) {
					AG_Delay(1);
					goto out;
				}
				dsw->rLast = t;
				
				doRedraw = (dsw->flags & AG_DRIVER_SW_REDRAW);
				if (doRedraw) {
					dsw->flags &= ~(AG_DRIVER_SW_REDRAW);
				} else {
					AG_FOREACH_WINDOW(win, drv)
						if (win->visible && win->dirty)
							break;
				}
				if (doRedraw || win != NULL) {
					AG_BeginRendering(drv);
					AG_FOREACH_WINDOW(win, drv) {
						if (!win->visible) {
							continue;
						}
						AG_ObjectLock(win);
						AGDRIVER_CLASS(drv)->renderWindow(win);
						win->dirty = 0;
						AG_ObjectUnlock(win);
					}
					AG_EndRendering(drv);
				}
			}
			break;
		}
	}
out:
	AG_UnlockVFS(&agDrivers);
}

/*
 * Build an ordered list (with duplicates allowed) of all potentially
 * focusable widgets in a window. Honor focusFwd and "tabindex" attributes.
 */
static void
FindFocusableWidgets(AG_WidgetVec *W, AG_Widget *_Nonnull wid)
{
	AG_Widget *chld;
	
	AG_OBJECT_ISA(wid, "AG_Widget:*");

	AG_ObjectLock(wid);
	if (wid->flags & AG_WIDGET_FOCUSABLE)
		AG_VEC_PUSH(W, wid);
#if 0
	if (AG_Defined(wid, "tabindex"))
		/* TODO move to proper index. */
#endif
	OBJECT_FOREACH_CHILD(chld, wid, ag_widget) {
		if (AG_WINDOW_ISA(chld)) {
			continue;
		}
		FindFocusableWidgets(W, chld);
	}
	AG_ObjectUnlock(wid);
}

/*
 * Move the widget focus inside a window.
 * The agDrivers VFS must be locked.
 */
void
AG_WindowCycleFocus(AG_Window *win, int reverse)
{
	AG_WidgetVec W;		/* Ordered list of potential candidates */
	AG_WidgetVec WU;	/* W with duplicates removed */
	AG_Widget *chld;
	int i, j;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");

	AG_VEC_INIT(&W);
	AG_VEC_INIT(&WU);

	OBJECT_FOREACH_CHILD(chld, win, ag_widget) {
		if (AG_WINDOW_ISA(chld)) {
			continue;
		}
		FindFocusableWidgets(&W, chld);
	}

	for (i = 0; i < W.length; i++) {
		for (j = 0; j < WU.length; j++) {
			if (W.data[i] == WU.data[j])
				break;
		}
		if (j == WU.length)
			AG_VEC_PUSH(&WU, W.data[i]);
	}
	if (WU.length == 0)
		goto out;

	/* Move focus after/before the currently focused widget. */
	if (reverse) {
		for (i = 0; i < WU.length; i++) {
			if (WIDGET(WU.data[i])->flags & AG_WIDGET_FOCUSED)
				break;
		}
		if (i == -1) {
			AG_WidgetFocus(WU.data[0]);
		} else {
			if (i-1 < 0) {
				AG_WidgetFocus(WU.data[WU.length-1]);
			} else {
				AG_WidgetFocus(WU.data[i-1]);
			}
		}
	} else {
		for (i = WU.length-1; i >= 0; i--) {
			if (WIDGET(WU.data[i])->flags & AG_WIDGET_FOCUSED)
				break;
		}
		if (i == WU.length) {
			AG_WidgetFocus(WU.data[0]);
		} else {
			if (i+1 < WU.length) {
				AG_WidgetFocus(WU.data[i+1]);
			} else {
				AG_WidgetFocus(WU.data[0]);
			}
		}
	}
out:
	AG_VEC_DESTROY(&WU);
	AG_VEC_DESTROY(&W);
	win->dirty = 1;
}

/*
 * Return a pointer to the currently focused window.
 * The agDrivers VFS must be locked.
 */
AG_Window *
AG_WindowFindFocused(void)
{
	return (agWindowFocused);
}

/*
 * Test whether the given window is focused.
 * The agDrivers VFS must be locked.
 */
int
AG_WindowIsFocused(AG_Window *_Nullable win)
{
	return (agWindowFocused == win);
}

/*
 * Give input focus to a window. The actual focus change will take effect at
 * the end of the current event cycle.
 */
void
AG_WindowFocus(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");

	AG_LockVFS(&agDrivers);

	if (win == NULL || agWindowFocused == win)
		goto out;

	/* Abort if there are any visible modal windows. */
	if (agDriverOps->wm == AG_WM_MULTIPLE) {
		AG_Driver *drv;
		AG_Window *other;

		AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
			AG_OBJECT_ISA(drv, "AG_Driver:*");
			AG_FOREACH_WINDOW(other, drv) {
				if (other == win) {
					continue;
				}
				AG_OBJECT_ISA(other, "AG_Widget:AG_Window:*");
				AG_ObjectLock(other);
				if (other->flags & AG_WINDOW_MODAL) {
					AG_ObjectUnlock(other);
					goto out;
				}
				AG_ObjectUnlock(other);
			}
		}
	}

	AG_ObjectLock(win);
	if (win->flags & AG_WINDOW_DENYFOCUS) {
		AG_ObjectUnlock(win);
		goto out;
	}
	if (OBJECT(win)->parent == NULL) {
		/* Will focus on future attach */
		win->flags |= AG_WINDOW_FOCUSONATTACH;
	} else {
		agWindowToFocus = win;
	}
	win->dirty = 1;
	AG_ObjectUnlock(win);
out:
	AG_UnlockVFS(&agDrivers);
}

/*
 * Give focus to a window by name, and show it is if is hidden.
 */
int
AG_WindowFocusNamed(const char *name)
{
	AG_Driver *drv;
	AG_Window *owin;
	int rv = 0;

	AG_LockVFS(&agDrivers);

	AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
		AG_OBJECT_ISA(drv, "AG_Driver:*");
		AG_FOREACH_WINDOW(owin, drv) {
			AG_OBJECT_ISA(owin, "AG_Widget:AG_Window:*");
			if (strcmp(OBJECT(owin)->name, name) == 0) {
				AG_WindowShow(owin);
				AG_WindowFocus(owin);
			    	rv = 1;
				goto out;
			}
		}
	}
out:
	AG_UnlockVFS(&agDrivers);
	return (rv);
}

/*
 * Focus the window at display coordinates x,y (single-window drivers only).
 * Returns 1 if a candidate has been found, otherwise return 0.
 */
int
AG_WindowFocusAtPos(AG_DriverSw *dsw, int x, int y)
{
	AG_Window *win;
	
	AG_LockVFS(&agDrivers);
	AG_OBJECT_ISA(dsw, "AG_Driver:AG_DriverSw:*");

	AG_FOREACH_WINDOW_REVERSE(win, dsw) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");

		if (!win->visible)
			continue;

		AG_ObjectLock(win);
		if (!AG_WidgetArea(win, x,y) ||
		    (win->flags & AG_WINDOW_DENYFOCUS)) {
			AG_ObjectUnlock(win);
			continue;
		}
		agWindowToFocus = win;                      /* Change focus */
		AG_ObjectUnlock(win);

		AG_UnlockVFS(&agDrivers);
		return (1);
	}
	AG_UnlockVFS(&agDrivers);
	return (0);
}

/* Update window background after geometry change in single-display mode. */
/* XXX TODO Avoid drawing over KEEPABOVE windows */
static void
UpdateWindowBG(AG_Window *_Nonnull win, const AG_Rect *_Nonnull rPrev)
{
	AG_Driver *drv = WIDGET(win)->drv;
	AG_Rect r;

	if (WIDGET(win)->x > rPrev->x) {		/* L-resize */
		r.x = rPrev->x;
		r.y = rPrev->y;
		r.w = WIDGET(win)->x - rPrev->x;
		r.h = HEIGHT(win);
	} else if (WIDTH(win) < rPrev->w) {		/* R-resize */
		r.x = WIDGET(win)->x + WIDTH(win);
		r.y = WIDGET(win)->y;
		r.w = rPrev->w - WIDTH(win) + 1;
		r.h = rPrev->h + 1;
	} else {
		r.w = 0;
		r.h = 0;
	}
	if (r.w > 0 && r.h > 0) {
		AGDRIVER_CLASS(drv)->fillRect(drv, &r, &AGDRIVERSW(drv)->bgColor);
		if (AGDRIVER_CLASS(drv)->updateRegion != NULL)
			AGDRIVER_CLASS(drv)->updateRegion(drv, &r);
	}
	if (HEIGHT(win) < rPrev->h) {				/* H-resize */
		r.x = rPrev->x;
		r.y = WIDGET(win)->y + HEIGHT(win);
		r.w = rPrev->w;
		r.h = rPrev->h - HEIGHT(win) + 1;
			
		AGDRIVER_CLASS(drv)->fillRect(drv, &r, &AGDRIVERSW(drv)->bgColor);

		if (AGDRIVER_CLASS(drv)->updateRegion != NULL)
			AGDRIVER_CLASS(drv)->updateRegion(drv, &r);
	}
}

/*
 * Set the coordinates and geometry of a window.
 *
 * If r.x or r.y is -1 then the window is auto-placed (according to the
 * AG_WindowSetPosition() settings but subject to native WM limitations).
 *
 * If r.w or r.w is -1 then the window size is set automatically (according
 * to the size requisition of the currently attached widgets).
 */
int
AG_WindowSetGeometry(AG_Window *win, int x, int y, int w, int h)
{
	AG_Rect r;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	r.x = x;
	r.y = y;
	r.w = w;
	r.h = h;
	return AG_WindowSetGeometryRect(win, &r, 0);
}

/*
 * Set the coordinates and geometry of a window.
 *
 * If r.x or r.y is -1 then the window is auto-placed (according to the
 * AG_WindowSetPosition() settings but subject to native WM limitations).
 *
 * If r.w or r.w is -1 then the window size is set automatically (according
 * to the size requisition of the currently attached widgets).
 * 
 * If bounded is 1 then the window geometry (or position) is clamped so that
 * the window will not exceed the boundaries of the display.
 */
int
AG_WindowSetGeometryRect(AG_Window *win, const AG_Rect *r, int bounded)
{
	AG_Driver *drv = WIDGET(win)->drv;
	const AG_DriverClass *dc = AGDRIVER_CLASS(drv);
	AG_SizeReq rWin;
	AG_SizeAlloc a;
	AG_Rect rPrev;
	int autoplace, nw, nh, wMin, hMin, paddingHoriz, paddingVert;
	Uint wDisp = 0, hDisp = 0;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(drv, "AG_Driver:*");
	AG_ObjectLock(win);

	rPrev.x = WIDGET(win)->x;
	rPrev.y = WIDGET(win)->y;
	rPrev.w = WIDGET(win)->w;
	rPrev.h = WIDGET(win)->h;
	autoplace = ((WIDGET(win)->x == -1 || WIDGET(win)->y == -1));

	if (r->w == -1 || r->h == -1) {                         /* Autosize */
		AG_WidgetSizeReq(win, &rWin);
		nw = (r->w == -1) ? rWin.w : r->w;
		nh = (r->h == -1) ? rWin.h : r->h;
	} else {
		nw = r->w;
		nh = r->h;
	}

	if (win->flags & AG_WINDOW_MINSIZEPCT) {        /* [wh]Min are in % */
		wMin = win->minPct * r->w / 100;
		hMin = win->minPct * r->h / 100;
	} else {
		wMin = win->wMin;
		hMin = win->hMin;
	}

	paddingHoriz = WIDGET(win)->paddingLeft + WIDGET(win)->paddingRight;
	paddingVert = WIDGET(win)->paddingTop + WIDGET(win)->paddingBottom;

	if (nw < paddingHoriz + wMin) { nw = paddingHoriz + wMin; }
	if (nh < paddingVert + hMin) { nh = paddingVert + hMin; }

	if (WIDGET(win)->x == -1 ||
	    WIDGET(win)->y == -1) {
		if (AG_GetDisplaySize(drv, &wDisp, &hDisp) == -1) {
			wDisp = 0;
			hDisp = 0;
		}
		if (WIDGET(win)->x == -1)
			WIDGET(win)->x = (wDisp >> 1) - (nw >> 1);
		if (WIDGET(win)->y == -1)
			WIDGET(win)->y = (hDisp >> 1) - (nh >> 1);
	}
	a.x = (r->x == -1) ? WIDGET(win)->x : r->x;
	a.y = (r->y == -1) ? WIDGET(win)->y : r->y;
	a.w = nw;
	a.h = nh;
	if (bounded)
		AG_WM_LimitWindowToDisplaySize(drv, &a);

	/* Invoke the driver-specific pre-resize callback routine. */
	if (win->visible && AGDRIVER_MULTIPLE(drv))
		AGDRIVER_MW_CLASS(drv)->preResizeCallback(win);
	
	/*
	 * Resize the widgets and update their coordinates; update
	 * the geometry of the window's Widget structure.
	 */
	AG_WidgetSizeAlloc(win, &a);
	AG_WidgetUpdateCoords(win, a.x, a.y);

	switch (AGDRIVER_CLASS(drv)->wm) {
	case AG_WM_SINGLE:
		if (dc->type == AG_FRAMEBUFFER && win->visible && !autoplace) {
			UpdateWindowBG(win, &rPrev);
		}
		break;
	case AG_WM_MULTIPLE:
		if (AGDRIVERMW(drv)->flags & AG_DRIVER_MW_OPEN) {
			if (AGDRIVER_MW_CLASS(drv)->moveResizeWindow(win, &a) == -1)
				goto fail;
		}
		break;
	}

	win->dirty = 1;

	AG_ObjectUnlock(win);
	return (0);
fail:
	if (!autoplace) {                                         /* Revert */
		a.x = rPrev.x;
		a.y = rPrev.y;
		a.w = rPrev.w;
		a.h = rPrev.h;
		AG_WidgetSizeAlloc(win, &a);
		AG_WidgetUpdateCoords(win, a.x, a.y);
	}
	AG_ObjectUnlock(win);
	return (-1);
}

/* Move a window by relative distances in pixels. */
int
AG_WindowMove(AG_Window *win, int xRel, int yRel)
{
	AG_Driver *drv = WIDGET(win)->drv;
	const AG_DriverClass *dc = AGDRIVER_CLASS(drv);
	AG_Rect rPrev;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(drv, "AG_Driver:*");
	AG_ObjectLock(win);

	rPrev.x = WIDGET(win)->x;
	rPrev.y = WIDGET(win)->y;
	rPrev.w = WIDGET(win)->w;
	rPrev.h = WIDGET(win)->h;

	AG_WidgetUpdateCoords(win,
	    WIDGET(win)->x + xRel,
	    WIDGET(win)->y + yRel);

	switch (AGDRIVER_CLASS(drv)->wm) {
	case AG_WM_SINGLE:
		WIDGET(win)->x += xRel;
		WIDGET(win)->y += yRel;

		if (dc->type == AG_FRAMEBUFFER && win->visible) {
			UpdateWindowBG(win, &rPrev);
		}
		break;
	case AG_WM_MULTIPLE:
		if ((AGDRIVERMW(drv)->flags & AG_DRIVER_MW_OPEN) &&
		    AGDRIVER_MW_CLASS(drv)->moveWindow(win,
		    WIDGET(win)->x + xRel,
		    WIDGET(win)->y + yRel) == -1) {
			goto fail;
		}
		break;
	}

	win->dirty = 1;
	AG_ObjectUnlock(win);
	return (0);
fail:
	AG_ObjectUnlock(win);
	return (-1);
}

/* Configure minimum window size in percentage of computed geometry. */
void
AG_WindowSetMinSizePct(AG_Window *win, int pct)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	win->flags |= AG_WINDOW_MINSIZEPCT;
	win->minPct = pct;
	win->dirty = 1;

	AG_ObjectUnlock(win);
}

/* Configure minimum window size in pixels. */
void
AG_WindowSetMinSize(AG_Window *win, int w, int h)
{
	AG_Driver *drv;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	win->flags &= ~(AG_WINDOW_MINSIZEPCT);
	win->wMin = w;
	win->hMin = h;
	win->dirty = 1;

	drv = WIDGET(win)->drv;
	if (AGDRIVER_MULTIPLE(drv) &&
	    (AGDRIVERMW(drv)->flags & AG_DRIVER_MW_OPEN) &&
	    AGDRIVER_MW_CLASS(drv)->setWindowMinSize != NULL) {
		AGDRIVER_MW_CLASS(drv)->setWindowMinSize(win, w,h);
	}
	AG_ObjectUnlock(win);
}

/*
 * Update the x,y of the a to produce the required alignment for the
 * window's current display.
 *
 * The window must be locked.
 */
void
AG_WindowComputeAlignment(AG_Window *win, AG_SizeAlloc *a)
{
	AG_Driver *drv = WIDGET(win)->drv;
	Uint wMax, hMax;
	int w = a->w;
	int h = a->h;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_OBJECT_ISA(drv, "AG_Driver:*");

	if (AG_GetDisplaySize(WIDGET(win)->drv, &wMax, &hMax) == -1)
		return;

	switch (win->alignment) {
	case AG_WINDOW_TL:
		a->x = 0;
		a->y = 0;
		break;
	case AG_WINDOW_TC:
		a->x = (wMax >> 1) - (w >> 1);
		a->y = 0;
		break;
	case AG_WINDOW_TR:
		a->x = wMax - w;
		a->y = 0;
		break;
	case AG_WINDOW_ML:
		a->x = 0;
		a->y = (hMax >> 1) - (h >> 1);
		break;
	case AG_WINDOW_MR:
		a->x = wMax - w;
		a->y = (hMax >> 1) - (h >> 1);
		break;
	case AG_WINDOW_BL:
		a->x = 0;
		a->y = hMax - h;
		break;
	case AG_WINDOW_BC:
		a->x = (wMax >> 1) - (w >> 1);
		a->y = hMax - h;
		break;
	case AG_WINDOW_BR:
		a->x = wMax - w;
		a->y = hMax - h;
		break;
	case AG_WINDOW_MC:
	case AG_WINDOW_ALIGNMENT_NONE:
	default:
		a->x = (wMax >> 1) - (w >> 1);
		a->y = (hMax >> 1) - (h >> 1);
		break;
	}
	if (AGDRIVER_MULTIPLE(drv) &&
	    AGDRIVER_MW_CLASS(drv)->tweakAlignment != NULL)
		AGDRIVER_MW_CLASS(drv)->tweakAlignment(win, a, wMax, hMax);
}

/* Assign a window a specific alignment and size in pixels. */
int
AG_WindowSetGeometryAligned(AG_Window *win, enum ag_window_alignment alignment,
    int w, int h)
{
	AG_SizeAlloc a;
	int rv;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	win->alignment = alignment;

	a.x = 0;
	a.y = 0;
	a.w = (w != -1) ? w : WIDGET(win)->w;
	a.h = (h != -1) ? h : WIDGET(win)->h;
	AG_WindowComputeAlignment(win, &a);

	rv = AG_WindowSetGeometry(win, a.x, a.y, a.w, a.h);

	AG_ObjectUnlock(win);
	return (rv);
}

/* Assign a window a specific alignment and size in percentage of view area. */
int
AG_WindowSetGeometryAlignedPct(AG_Window *win, enum ag_window_alignment align,
    int wPct, int hPct)
{
	Uint wMax=0, hMax=0;
	AG_Driver *drv;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	drv = WIDGET(win)->drv;
	AG_OBJECT_ISA(drv, "AG_Driver:*");

	AG_GetDisplaySize(drv, &wMax, &hMax);

	return AG_WindowSetGeometryAligned(win, align, wPct * wMax / 100,
	                                               hPct * hMax / 100);
}

/* Save the current window geometry prior to Maximize or Minimize. */
void
AG_WindowSaveGeometry(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");

	win->rSaved.x = WIDGET(win)->x;
	win->rSaved.y = WIDGET(win)->y;
	win->rSaved.w = WIDTH(win);
	win->rSaved.h = HEIGHT(win);
}

/* Restore saved geometry (i.e., after an unminimize operation) */
int
AG_WindowRestoreGeometry(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");

	return AG_WindowSetGeometryRect(win, &win->rSaved, 0);
}

/* Maximize a window */
void
AG_WindowMaximize(AG_Window *win)
{
	Uint wMax, hMax;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	AG_WindowSaveGeometry(win);
	AG_GetDisplaySize(WIDGET(win)->drv, &wMax, &hMax);

	if (AG_WindowSetGeometry(win, 0, 0, wMax, hMax) == 0)
		win->flags |= AG_WINDOW_MAXIMIZED;
	
	AG_ObjectUnlock(win);
}

/* Restore a window's geometry prior to maximization. */
void
AG_WindowUnmaximize(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	if (AG_WindowRestoreGeometry(win) == 0) {
		win->flags &= ~(AG_WINDOW_MAXIMIZED);
		win->dirty = 1;
	}
	
	AG_ObjectUnlock(win);
}

#ifdef AG_WIDGETS
/*
 * Drag-and-droppable AG_Icon(3) are used to represent minimized windows
 * in single-window mode. Set event handlers to unminimize on double-click.
 */
static void
IconMouseMotion(AG_Event *_Nonnull event)
{
	AG_Icon *icon = AG_ICON_SELF();
	AG_Driver *drv = WIDGET(icon)->drv;
	int xRel = AG_INT(3);
	int yRel = AG_INT(4);

	AG_OBJECT_ISA(drv, "AG_Driver:*");

	if (icon->flags & AG_ICON_DND) {
		AG_Rect r;
		AG_Window *wDND = icon->wDND;

		AG_OBJECT_ISA(wDND, "AG_Widget:AG_Window:*");

		if (drv && AGDRIVER_SINGLE(drv)) {         /* Must clear BG */
			r.x = 0;
			r.y = 0;
			r.w = AGDRIVERSW(drv)->w;
			r.h = AGDRIVERSW(drv)->h;

			AGDRIVER_CLASS(drv)->fillRect(drv, &r,
			    &AGDRIVERSW(drv)->bgColor);

			AG_Rect2ToRect(&r, &WIDGET(wDND)->rView);

			if (AGDRIVER_CLASS(drv)->updateRegion != NULL)
				AGDRIVER_CLASS(drv)->updateRegion(drv, &r);
		}
		r.x = WIDGET(wDND)->x + xRel;
		r.y = WIDGET(wDND)->y + yRel;
		r.w = WIDTH(wDND);
		r.h = HEIGHT(wDND);
		AG_WindowSetGeometryRect(wDND, &r, 0);
		 
		icon->xSaved = WIDGET(wDND)->x;
		icon->ySaved = WIDGET(wDND)->y;
		icon->wSaved = WIDTH(wDND);
		icon->hSaved = HEIGHT(wDND);
	}
}

static Uint32
IconDoubleClickTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event)
{
	AG_Icon *icon = AG_ICON_SELF();

	icon->flags &= ~(AG_ICON_DBLCLICKED);
	return (0);
}

static void
IconMouseButtonDown(AG_Event *_Nonnull event)
{
	AG_Icon *icon = AG_ICON_SELF();

	WIDGET(icon)->flags |= AG_WIDGET_UNFOCUSED_MOTION |
	                       AG_WIDGET_UNFOCUSED_BUTTONUP;

	if (icon->flags & AG_ICON_DBLCLICKED) {
		AG_Window *win = AG_WINDOW_PTR(1);

		AG_DelTimer(icon, &icon->toDblClick);
		AG_WindowUnminimize(win);
		icon->flags &= (AG_ICON_DND | AG_ICON_DBLCLICKED);
	} else {
		icon->flags |= (AG_ICON_DND | AG_ICON_DBLCLICKED);
		AG_AddTimer(icon, &icon->toDblClick, agMouseDblclickDelay,
		    IconDoubleClickTimeout, NULL);
	}
}

static void
IconMouseButtonUp(AG_Event *_Nonnull event)
{
	AG_Icon *icon = AG_ICON_SELF();
	
	WIDGET(icon)->flags &= ~(AG_WIDGET_UNFOCUSED_MOTION);
	WIDGET(icon)->flags &= ~(AG_WIDGET_UNFOCUSED_BUTTONUP);
	icon->flags &= ~(AG_ICON_DND);
}

static int
CreateMinimizedIcon(AG_Window *_Nonnull win)
{
	AG_Window *wDND;
	AG_Icon *icon = win->icon;

	AG_OBJECT_ISA(icon, "AG_Widget:AG_Icon:*");

	if ((wDND = icon->wDND) == NULL) {
		wDND = icon->wDND = AG_WindowNew(AG_WINDOW_PLAIN |
		                                 AG_WINDOW_KEEPBELOW |
		                                 AG_WINDOW_DENYFOCUS |
		                                 AG_WINDOW_NOBACKGROUND);
		if (wDND == NULL) {
			return (-1);
		}
		wDND->wmType = AG_WINDOW_WM_DND;
		AG_ObjectAttach(wDND, icon);
		AG_ObjectSetName(wDND, "icon%u", agWindowIconCounter++);
	} else {
		AG_WindowShow(wDND);
	}

	icon->flags &= ~(AG_ICON_DND | AG_ICON_DBLCLICKED);

	AG_SetEvent(icon, "mouse-motion", IconMouseMotion, NULL);
	AG_SetEvent(icon, "mouse-button-up", IconMouseButtonUp, NULL);
	AG_SetEvent(icon, "mouse-button-down", IconMouseButtonDown, "%p", win);

	if (icon->xSaved != -1) {
		AG_WindowShow(wDND);
		AG_WindowSetGeometry(wDND, icon->xSaved, icon->ySaved,
		                           icon->wSaved, icon->hSaved);
	} else {
		AG_WindowSetPosition(wDND, AG_WINDOW_LOWER_LEFT, 1);
		AG_WindowShow(wDND);

		icon->xSaved = WIDGET(wDND)->x;
		icon->ySaved = WIDGET(wDND)->y;
		icon->wSaved = WIDTH(wDND);
		icon->hSaved = HEIGHT(wDND);
	}
	return (0);
}

static void
HideMinimizedIcon(AG_Window *_Nonnull win)
{
	AG_Icon *icon;

	if ((icon = win->icon) == NULL) {
		return;
	}
	AG_OBJECT_ISA(icon, "AG_Widget:AG_Icon:*");
	if (icon->wDND) {
		AG_OBJECT_ISA(icon->wDND, "AG_Widget:AG_Window:*");
		AG_WindowHide(icon->wDND);
	}
}
#endif /* AG_WIDGETS */

/* Minimize a window */
void
AG_WindowMinimize(AG_Window *win)
{
	AG_Driver *drv;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	if (win->flags & AG_WINDOW_MINIMIZED) {
		goto out;
	}
	win->flags |= AG_WINDOW_MINIMIZED;
	AG_WindowHide(win);
#ifdef AG_WIDGETS
	if ((drv = WIDGET(win)->drv) && AGDRIVER_SINGLE(drv)) {
		if (CreateMinimizedIcon(win) == -1)
			goto out;
	}
#endif
	/* TODO MW: send a WM_CHANGE_STATE */
out:
	AG_ObjectUnlock(win);
}

/* Unminimize a window */
void
AG_WindowUnminimize(AG_Window *win)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	if (!win->visible) {
		AG_Driver *drv;
#ifdef AG_WIDGETS
		if ((drv = WIDGET(win)->drv) && AGDRIVER_SINGLE(drv))
			HideMinimizedIcon(win);
#endif
		AG_WindowShow(win);
		win->flags &= ~(AG_WINDOW_MINIMIZED);
	} else {
		AG_WindowFocus(win);
	}
	/* TODO MW: send a WM_CHANGE_STATE */
	AG_ObjectUnlock(win);
}

/* AGWINDETACH(): General-purpose "detach window" event handler. */
void
AG_WindowDetachGenEv(AG_Event *event)
{
	AG_ObjectDetach(AG_WINDOW_PTR(1));
}

/* AGWINHIDE(): General-purpose "hide window" event handler. */
void
AG_WindowHideGenEv(AG_Event *event)
{
	AG_WindowHide(AG_WINDOW_PTR(1));
}

/* AGWINCLOSE(): General-purpose "close window" event handler. */
void
AG_WindowCloseGenEv(AG_Event *event)
{
	AG_PostEvent(AG_WINDOW_PTR(1), "window-close", NULL);
}

/* Close the actively focused window. */
void
AG_CloseFocusedWindow(void)
{
	AG_LockVFS(&agDrivers);
	if (agWindowFocused) {
		AG_OBJECT_ISA(agWindowFocused, "AG_Widget:AG_Window:*");
		AG_PostEvent(agWindowFocused, "window-close", NULL);
	}
	AG_UnlockVFS(&agDrivers);
}

static void
SizeRequest(void *_Nonnull obj, AG_SizeReq *_Nonnull r)
{
	AG_Window *win = obj;
	AG_Widget *chld;
	AG_Titlebar *tbar = win->tbar;
	AG_SizeReq req;
	const int spacingVert = WIDGET(win)->spacingVert;
	const int wPadding = WIDGET(win)->paddingLeft + (win->wBorderSide << 1) +
	                     WIDGET(win)->paddingRight;

	r->w = wPadding;
	r->h = WIDGET(win)->paddingBottom + win->wBorderBot +
	       WIDGET(win)->paddingTop;

	if (tbar) {
		AG_WidgetSizeReq(tbar, &req);
		r->w = MAX(r->w, req.w);
		r->h += req.h;
	}
	OBJECT_FOREACH_CHILD(chld, win, ag_widget) {
		if (chld == WIDGET(tbar)) {
			continue;
		}
		AG_OBJECT_ISA(chld, "AG_Widget:*");

		AG_WidgetSizeReq(chld, &req);
		req.w += (chld->marginLeft + chld->marginRight);
		req.h += (chld->marginTop + chld->marginBottom);

		r->w = MAX(r->w, req.w + wPadding);
		r->h += (req.h + spacingVert);
	}
	if (r->h >= spacingVert)
		r->h -= spacingVert;
}

static int
SizeAllocate(void *_Nonnull obj, const AG_SizeAlloc *_Nonnull a)
{
	AG_Window *win = obj;
	AG_Widget *chld;
	AG_SizeReq req;
	AG_SizeAlloc alloc;
	AG_Rect r;
	const int spacing = WIDGET(win)->spacingVert;
	const int wBorderSide = win->wBorderSide;
	const int wBorderBot = win->wBorderBot;
	int wAvail, hAvail, hReqd, x, y;

	/*
	 * Set cursor-change areas over the window resize controls
	 * (used with single-window drivers).
	 */
	if (wBorderSide > 0) {
		r.x = 0;
		r.y = 0;
		r.w = wBorderSide;
		r.h = a->h - wBorderBot;
		AG_SetStockCursor(win, &win->pvt.caResize[0], &r,
		    AG_HRESIZE_CURSOR);
		r.x = a->w - wBorderSide;
		AG_SetStockCursor(win, &win->pvt.caResize[4], &r,
		    AG_HRESIZE_CURSOR);
	}
	if (wBorderBot > 0) {
		int wResizeCtrl = win->wResizeCtrl;

		r.x = 0;
		r.y = a->h - wBorderBot;
		r.w = wResizeCtrl;
		r.h = wBorderBot;
		AG_SetStockCursor(win, &win->pvt.caResize[1], &r,
		    AG_LRDIAG_CURSOR);

		r.x = wResizeCtrl;
		r.w = a->w - (wResizeCtrl << 1);
		AG_SetStockCursor(win, &win->pvt.caResize[2], &r,
		    AG_VRESIZE_CURSOR);

		r.x = a->w - wResizeCtrl;
		r.w = wResizeCtrl;
		AG_SetStockCursor(win, &win->pvt.caResize[3], &r,
		    AG_LLDIAG_CURSOR);
	}

	/*
	 * Widget size requisition. Get the sum height in pixels requested
	 * by (non-VFILL) child widgets. Allocate the space to the widgets
	 * and the remainder to the VFILL widget if one exists.
	 */

	wAvail = a->w - WIDGET(win)->paddingLeft - WIDGET(win)->paddingRight -
	         (wBorderSide << 1);
	hAvail = a->h - WIDGET(win)->paddingTop - WIDGET(win)->paddingBottom -
	         wBorderBot;
	hReqd = 0;
	OBJECT_FOREACH_CHILD(chld, win, ag_widget) {
		AG_OBJECT_ISA(chld, "AG_Widget:*");

		AG_WidgetSizeReq(chld, &req);
		req.h += chld->marginTop + chld->marginBottom;

		if ((chld->flags & AG_WIDGET_VFILL) == 0) {
			hReqd += req.h;
		}
		if (chld != WIDGET(win->tbar))
			hReqd += spacing;
	}
	if (hReqd >= spacing)
		hReqd -= spacing;

	/* Size and position the child widgets. */
	x = WIDGET(win)->paddingLeft + wBorderSide;
	y = 0;

	OBJECT_FOREACH_CHILD(chld, win, ag_widget) {
		Uint flags;

		AG_OBJECT_ISA(chld, "AG_Widget:*");

		AG_WidgetSizeReq(chld, &req);

		if (chld == WIDGET(win->tbar)) {     /* Titlebar (no spacing) */
			alloc.x = 0;
			alloc.y = 0;
			alloc.w = a->w;
			alloc.h = req.h;
			AG_WidgetSizeAlloc(chld, &alloc);
			y += alloc.h;
			continue;
		}

		flags = chld->flags;

		alloc.x = x + chld->marginLeft;
		alloc.y = y + chld->marginTop;
		alloc.w = (flags & AG_WIDGET_HFILL) ?
		          (wAvail - chld->marginLeft - chld->marginRight) :
			  MIN(wAvail, req.w);
		alloc.h = (flags & AG_WIDGET_VFILL) ?
		          (hAvail - hReqd) :
			  req.h;

		AG_WidgetSizeAlloc(chld, &alloc);

		y += alloc.h + spacing + chld->marginTop + chld->marginBottom;
	}

	win->r.x = 0;
	win->r.y = 0;
	win->r.w = a->w;
	win->r.h = a->h;
	return (0);
}

/* Set the width of the window side borders. */
void
AG_WindowSetSideBorders(AG_Window *win, int pixels)
{
	if (win) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		win->wBorderSide = pixels;
		win->dirty = 1;
	} else {
		agWindowSideBorderDefault = pixels;
	}
}

/* Set the width of the window bottom border. */
void
AG_WindowSetBottomBorder(AG_Window *win, int pixels)
{
	if (win) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		win->wBorderBot = pixels;
		win->dirty = 1;
	} else {
		agWindowBotBorderDefault = pixels;
	}
}

/* Request a specific initial window position. */
void
AG_WindowSetPosition(AG_Window *win, enum ag_window_alignment alignment,
    int tiling)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	win->alignment = alignment;

	AG_SETFLAGS(win->flags, AG_WINDOW_TILING, tiling);

	AG_ObjectUnlock(win);
}

/* Set a default window-close handler. */
void
AG_WindowSetCloseAction(AG_Window *win, enum ag_window_close_action mode)
{
	switch (mode) {
	case AG_WINDOW_HIDE:
		AG_SetEvent(win, "window-close", AGWINHIDE(win));
		break;
	case AG_WINDOW_DETACH:
		AG_SetEvent(win, "window-close", AGWINDETACH(win));
		break;
	default:
		AG_UnsetEvent(win, "window-close");
		break;
	}
}

/* Update Agar's built-in titlebar from window caption. */
static void
UpdateTitlebar(AG_Window *_Nonnull win)
{
	AG_Titlebar *tbar;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	tbar = win->tbar;
	AG_OBJECT_ISA(tbar, "AG_Widget:AG_Box:AG_Titlebar:*");
	AG_ObjectLock(tbar);
	AG_LabelTextS(tbar->label, win->caption);
	AG_ObjectUnlock(tbar);
}

/* Update the window's minimized icon caption. */
static void
UpdateIconCaption(AG_Window *_Nonnull win)
{
	AG_Icon *icon = win->icon;
	char s[20], *c;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	icon = win->icon;
	AG_OBJECT_ISA(icon, "AG_Widget:AG_Icon:*");

	if (Strlcpy(s, win->caption, sizeof(s)) >= sizeof(s)) {	/* Truncate */
		for (c = &s[0]; *c != '\0'; c++) {
			if (*c == ' ')
				*c = '\n';
		}
		AG_IconSetText(icon, "%s...", s);
	} else {
		AG_IconSetTextS(icon, s);
	}
}

/* Set the text to show inside a window's titlebar (format string). */
void
AG_WindowSetCaption(AG_Window *win, const char *fmt, ...)
{
	char *s;
	va_list ap;
	
	va_start(ap, fmt);
	Vasprintf(&s, fmt, ap);
	va_end(ap);

	AG_WindowSetCaptionS(win, s);

	free(s);
}

/* Set the text to show inside a window's titlebar (C string). */
void
AG_WindowSetCaptionS(AG_Window *win, const char *s)
{
	AG_Driver *drv;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	Strlcpy(win->caption, s, sizeof(win->caption));

	if (win->tbar)
		UpdateTitlebar(win);
	if (win->icon)
		UpdateIconCaption(win);

	if ((drv = WIDGET(win)->drv) && AGDRIVER_MULTIPLE(drv) &&
	    (AGDRIVERMW(drv)->flags & AG_DRIVER_MW_OPEN) &&
	    (AGDRIVER_MW_CLASS(drv)->setWindowCaption != NULL)) {
		AGDRIVER_MW_CLASS(drv)->setWindowCaption(win, s);
	} else {
		win->flags |= AG_WINDOW_UPDATECAPTION;      /* Update later */
	}

	win->dirty = 1;
	AG_ObjectUnlock(win);
}

/* Set the icon of a window from the contents of a surface. */
void
AG_WindowSetIcon(AG_Window *win, const AG_Surface *S)
{
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_IconSetSurface(win->icon, S);
}

/* Commit any pending focus change. The agDrivers VFS must be locked. */
void
AG_WindowProcessFocusChange(void)
{
	AG_Driver *drv;
	
	switch (agDriverOps->wm) {
	case AG_WM_SINGLE:
		AG_WM_CommitWindowFocus(agWindowToFocus);
		break;
	case AG_WM_MULTIPLE:
#ifdef AG_TYPE_SAFETY
		if (agWindowToFocus)
			AG_OBJECT_ISA(agWindowToFocus, "AG_Widget:AG_Window:*");
#endif
		if ((drv = WIDGET(agWindowToFocus)->drv) != NULL) {
			AG_OBJECT_ISA(drv, "AG_Driver:*");
			AGDRIVER_MW_CLASS(drv)->raiseWindow(agWindowToFocus);
			AGDRIVER_MW_CLASS(drv)->setInputFocus(agWindowToFocus);
		}
		break;
	}
	agWindowToFocus = NULL;
}

/*
 * Make windows on the show queue visible.
 * The agDrivers VFS must be locked.
 */
void
AG_WindowProcessShowQueue(void)
{
	AG_Window *win;

	TAILQ_FOREACH(win, &agWindowShowQ, pvt.visibility) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		AG_PostEvent(win, "widget-shown", NULL);
	}
	TAILQ_INIT(&agWindowShowQ);
}

/*
 * Make windows on the hide queue invisible.
 * The agDrivers VFS must be locked.
 */
void
AG_WindowProcessHideQueue(void)
{
	AG_Window *win;

	TAILQ_FOREACH(win, &agWindowHideQ, pvt.visibility) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		AG_PostEvent(win, "widget-hidden", NULL);
	}
	TAILQ_INIT(&agWindowHideQ);
}

/*
 * Close and destroy windows on the detach queue.
 * The agDrivers VFS must be locked.
 */
void
AG_WindowProcessDetachQueue(void)
{
	AG_Window *win, *winNext;
	AG_Driver *drv;
	int closedMain = 0, nHidden = 0;
#ifdef AG_DEBUG
	int debugLvlSave;
#endif
	TAILQ_FOREACH(win, &agWindowDetachQ, pvt.detach) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		if (!win->visible) {
			continue;
		}
		/*
		 * Note: `widget-hidden' event handlers may cause new windows
		 * to be added to agWindowDetachQ.
		 */
		AG_PostEvent(win, "widget-hidden", NULL);
		nHidden++;
	}
	if (nHidden > 0) {
		/*
		 * Windows were hidden. Defer detach operation until the next
		 * event cycle in case the underlying window system is unable
		 * to hide and unmap a window in the same event cycle.
		 */
		return;
	}

	for (win = TAILQ_FIRST(&agWindowDetachQ);
	     win != TAILQ_END(&agWindowDetachQ);
	     win = winNext) {
		winNext = TAILQ_NEXT(win, pvt.detach);
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		drv = WIDGET(win)->drv;
		AG_OBJECT_ISA(drv, "AG_Driver:*");

		/*
		 * Raise the window's "detached" event. The "detached" handlers
		 * of AG_Window and AG_Widget are both expected to recursively
		 * forward the "detached" event to their children.
		 */
		AG_PostEvent(win, "detached", "%p", drv);

		if (AGDRIVER_MULTIPLE(drv)) {
			if (AGDRIVERMW(drv)->flags & AG_DRIVER_MW_OPEN) {
				AGDRIVER_MW_CLASS(drv)->closeWindow(win);
				AGDRIVERMW(drv)->flags &= ~(AG_DRIVER_MW_OPEN);
			}
		} else {
			AG_UnmapAllCursors(win, win);
			win->tbar = NULL;
			AGDRIVERSW(drv)->flags |= AG_DRIVER_SW_REDRAW;
		}

		/* Unset detach-fn and perform a generic Object detach. */
		Debug_Mute(debugLvlSave);
		AG_SetFn(win, "detach-fn", NULL, NULL);
		Debug_Unmute(debugLvlSave);
		AG_ObjectDetachLockless(win);

		if (AGDRIVER_MULTIPLE(drv)) {
			/* Destroy the AG_Driver object. */
			AG_UnlockVFS(&agDrivers);
			AG_DriverClose(drv);
			AG_LockVFS(&agDrivers);
		}
		if (win->flags & AG_WINDOW_MAIN) {
			closedMain++;
		}
		AG_PostEvent(win, "window-detached", NULL);
		AG_ObjectDestroy(win);
	}
	TAILQ_INIT(&agWindowDetachQ);
	
#ifdef AG_EVENT_LOOP
	/* Terminate if the last AG_WINDOW_MAIN window was closed. */
	if (closedMain > 0) {
		AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) {
			AG_OBJECT_ISA(drv, "AG_Driver:*");
			AG_FOREACH_WINDOW(win, drv) {
				AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
				if (win->flags & AG_WINDOW_MAIN)
					break;
			}
			if (win != NULL)
				break;
		}
		if (drv == NULL) {
			AG_Terminate(0);
		}
	}
#endif /* AG_EVENT_LOOP */
}

/*
 * Configure a new cursor-change area with a specified cursor. The provided
 * cursor will be freed automatically on window detach.
 */
AG_CursorArea *
AG_MapCursor(void *obj, const AG_Rect *r, AG_Cursor *c)
{
	AG_Widget *wid = obj;
	AG_Window *win;
	AG_CursorArea *ca;
	
	AG_OBJECT_ISA(wid, "AG_Widget:*");

	if ((ca = TryMalloc(sizeof(AG_CursorArea))) == NULL) {
		return (NULL);
	}
	ca->stock = -1;
	ca->r = *r;
	ca->c = c;
	ca->wid = wid;
	
	if ((win = wid->window) != NULL) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		TAILQ_INSERT_TAIL(&win->pvt.cursorAreas, ca, cursorAreas);
	} else {
		TAILQ_INSERT_TAIL(&wid->pvt.cursorAreas, ca, cursorAreas);
	}
	return (ca);
}

/* Configure a new cursor-change area with a stock Agar cursor. */
AG_CursorArea *
AG_MapStockCursor(void *obj, const AG_Rect *_Nonnull r, int name)
{
	AG_Widget *wid = obj;
	AG_Window *win;
	AG_Driver *drv;
	AG_Cursor *ac;
	AG_CursorArea *ca;
	int i = 0;
	
	AG_OBJECT_ISA(wid, "AG_Widget:*");
	win = wid->window;
	drv = wid->drv;
	AG_OBJECT_ISA(drv, "AG_Driver:*");
	TAILQ_FOREACH(ac, &drv->cursors, cursors) {
		if (i++ == name)
			break;
	}
	if (ac == NULL)
		return (NULL);

	if ((ca = TryMalloc(sizeof(AG_CursorArea))) == NULL) {
		return (NULL);
	}
	ca->stock = name;
	ca->r = *r;
	ca->wid = WIDGET(obj);

	if (win != NULL) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		ca->c = ac;
		TAILQ_INSERT_TAIL(&win->pvt.cursorAreas, ca, cursorAreas);
	} else {
		/* Will resolve cursor name when widget is later attached. */
		ca->c = NULL;
		TAILQ_INSERT_TAIL(&wid->pvt.cursorAreas, ca, cursorAreas);
	}
	return (ca);
}

/*
 * Remove a cursor-change area and release the associated cursor (unless it
 * is a stock Agar cursor).
 */
void
AG_UnmapCursor(void *obj, AG_CursorArea *ca)
{
	AG_Widget *wid = obj;
	AG_Window *win = wid->window;

	if (win == NULL) {
		TAILQ_REMOVE(&wid->pvt.cursorAreas, ca, cursorAreas);
		free(ca);
	} else {
		AG_Driver *drv;
		
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		drv = WIDGET(win)->drv;
		AG_OBJECT_ISA(drv, "AG_Driver:*");

		if (ca->c == drv->activeCursor) {
			if (ca->stock == -1) {
				/* XXX TODO it would be safer to defer this operation */
				if (AGDRIVER_CLASS(drv)->unsetCursor != NULL) {
					AGDRIVER_CLASS(drv)->unsetCursor(drv);
				}
				AG_CursorFree(drv, ca->c);
			}
			TAILQ_REMOVE(&win->pvt.cursorAreas, ca, cursorAreas);
			free(ca);
		}
	}
}

/*
 * Destroy all cursors associated with a widget wid
 * (or all cursors altogether if wid is NULL).
 */
void
AG_UnmapAllCursors(AG_Window *win, void *wid)
{
	AG_Driver *drv;
	AG_CursorArea *ca, *caNext;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	drv = WIDGET(win)->drv;
	AG_OBJECT_ISA(drv, "AG_Driver:*");

	if (wid == NULL) {
		for (ca = TAILQ_FIRST(&win->pvt.cursorAreas);
		     ca != TAILQ_END(&win->pvt.cursorAreas);
		     ca = caNext) {
			caNext = TAILQ_NEXT(ca, cursorAreas);
			if (ca->stock == -1) {
				AG_CursorFree(drv, ca->c);
			}
			free(ca);
		}
		TAILQ_INIT(&win->pvt.cursorAreas);
	} else {
scan:
		TAILQ_FOREACH(ca, &win->pvt.cursorAreas, cursorAreas) {
			if (ca->wid != wid) {
				continue;
			}
			if (ca->stock == -1) {
				AG_CursorFree(drv, ca->c);
			}
			TAILQ_REMOVE(&win->pvt.cursorAreas, ca, cursorAreas);
			free(ca);
			goto scan;
		}
	}
}

/*
 * Set the per-window opacity (for compositing window managers).
 */
int
AG_WindowSetOpacity(AG_Window *win, float opacity)
{
	AG_Driver *drv;
	AG_WindowFadeCtx *fade;
	int rv = -1;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);
	drv = OBJECT(win)->parent;
	AG_OBJECT_ISA(drv, "AG_Driver:*");

	if (win->pvt.fade == NULL) {
		fade = win->pvt.fade = Malloc(sizeof(AG_WindowFadeCtx));
	} else {
		fade = win->pvt.fade;
	}
	fade->opacity = (opacity > 1.0f) ? 1.0f : opacity;

	if (drv && AGDRIVER_MULTIPLE(drv) &&
	    AGDRIVER_MW_CLASS(drv)->setOpacity != NULL)
		rv = AGDRIVER_MW_CLASS(drv)->setOpacity(win, fade->opacity);
	
	/* TODO: support compositing under single-window drivers. */
	AG_ObjectUnlock(win);
	return (rv);
}

/*
 * Set window fade-in/out parameters (for compositing window managers).
 */
void
AG_WindowSetFadeIn(AG_Window *win, float fadeTime, float fadeIncr)
{
	AG_WindowFadeCtx *fade;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	if (win->pvt.fade == NULL) {
		fade = win->pvt.fade = Malloc(sizeof(AG_WindowFadeCtx));
		fade->opacity = 1.0f;
	} else {
		fade = win->pvt.fade;
	}
	fade->inTime = fadeTime;
	fade->inIncr = fadeIncr;
	AG_InitTimer(&fade->timer, "fade", 0);
}
void
AG_WindowSetFadeOut(AG_Window *win, float fadeTime, float fadeIncr)
{
	AG_WindowFadeCtx *fade;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	if (win->pvt.fade == NULL) {
		fade = win->pvt.fade = Malloc(sizeof(AG_WindowFadeCtx));
		fade->opacity = 1.0f;
	} else {
		fade = win->pvt.fade;
	}
	fade->outTime = fadeTime;
	fade->outIncr = fadeIncr;
	AG_InitTimer(&fade->timer, "fade", 0);
}

/* Set the window zoom level. The scales are defined in agZoomValues[]. */
void
AG_WindowSetZoom(AG_Window *win, int zoom)
{
	AG_Window *winSub;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_ObjectLock(win);

	if (zoom < 0 || zoom >= AG_ZOOM_MAX || zoom == win->zoom) {
		AG_ObjectUnlock(win);
		return;
	}
	AG_SetStyleF(win, "font-size", "%.02f%%", agZoomValues[zoom]);

	win->zoom = zoom;

	AG_WindowUpdate(win);

	if (WIDGET(win)->drv) {
		AG_TextClearGlyphCache(WIDGET(win)->drv);
	}
	TAILQ_FOREACH(winSub, &win->pvt.subwins, pvt.swins) {
		if (winSub->flags & AG_WINDOW_INHERIT_ZOOM)
			AG_WindowSetZoom(winSub, zoom);
	}

	AG_ObjectUnlock(win);
}

/*
 * Render a window to the display (must be enclosed between calls to
 * AG_BeginRendering() and AG_EndRendering()).
 * 
 * The Window and agDrivers VFS must be locked.
 */
void
AG_WindowDraw(AG_Window *_Nonnull win)
{
	AG_Driver *drv = WIDGET(win)->drv;

	if (!win->visible)
		return;

	AG_OBJECT_ISA(drv, "AG_Driver:*");
	AGDRIVER_CLASS(drv)->renderWindow(win);

	win->dirty = 0;
}

/*
 * Recompute the coordinates and geometries of all widgets attached to the
 * window. This is used following AG_ObjectAttach() and AG_ObjectDetach()
 * calls made in event context, or direct modifications to the x,y,w,h
 * fields of the Widget structure.
 *
 * The agDrivers VFS and Window must be locked.
 */
void
AG_WindowUpdate(AG_Window *_Nonnull win)
{
	AG_SizeAlloc a;
	
	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");

	if (!AGDRIVER_SINGLE(WIDGET(win)->drv)) /* XXX jumpiness */
		AG_WidgetCompileStyle(win);
	
	if (WIDGET(win)->x != -1 && WIDGET(win)->y != -1) {
		a.x = WIDGET(win)->x;
		a.y = WIDGET(win)->y;
		a.w = WIDGET(win)->w;
		a.h = WIDGET(win)->h;
		AG_WidgetSizeAlloc(win, &a);
	}
	AG_WidgetUpdateCoords(win, WIDGET(win)->x, WIDGET(win)->y);
}

/*
 * Return visibility status of window.
 * The agDrivers VFS and Window object must be locked.
 */
int
AG_WindowIsVisible(AG_Window *_Nonnull win)
{
	return (win->visible);
}

/*
 * Return a pointer to a widget's parent window.
 * The agDrivers VFS must be locked.
 */
AG_Window *
AG_ParentWindow(void *_Nonnull obj)
{
	AG_OBJECT_ISA(obj, "AG_Widget:*");
	return (WIDGET(obj)->window);
}

/* Set an explicit widget position in pixels. */
void
AG_WidgetSetPosition(void *_Nonnull obj, int x, int y)
{
	AG_Widget *wid = obj;

	AG_OBJECT_ISA(wid, "AG_Widget:*");
	AG_ObjectLock(wid);
	wid->x = x;
	wid->y = y;
	wid->flags |= AG_WIDGET_UPDATE_WINDOW;
	AG_ObjectUnlock(wid);
}

/* Set an explicit widget geometry in pixels. */
void
AG_WidgetSetSize(void *_Nonnull obj, int w, int h)
{
	AG_Widget *wid = obj;

	AG_OBJECT_ISA(wid, "AG_Widget:*");
	AG_ObjectLock(wid);
	wid->w = w;
	wid->h = h;
	wid->flags |= AG_WIDGET_UPDATE_WINDOW;
	AG_ObjectUnlock(wid);
}

/* Set an explicit widget geometry from an AG_Rect argument. */
void
AG_WidgetSetGeometry(void *obj, const AG_Rect *r)
{
	AG_Widget *wid = obj;

	AG_OBJECT_ISA(wid, "AG_Widget:*");
	AG_ObjectLock(wid);
	wid->x = r->x;
	wid->y = r->y;
	wid->w = r->w;
	wid->h = r->h;
	wid->flags |= AG_WIDGET_UPDATE_WINDOW;
	AG_ObjectUnlock(wid);
}

/* Set the largest allowable window size. */
void
AG_WindowSetGeometryMax(AG_Window *_Nonnull win)
{
	Uint wMax, hMax;

	AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
	AG_GetDisplaySize((void *)WIDGET(win)->drv, &wMax, &hMax);
	AG_WindowSetGeometry(win, 0, 0, wMax, hMax);
}

/* Request widget redraw. */
void
AG_Redraw(void *_Nonnull obj)
{
	AG_Window *win;

	AG_OBJECT_ISA(obj, "AG_Widget:*");
#ifdef DEBUG_REDRAW
	Debug(obj, "Redraw %s @ t=%u\n",
	    WIDGET(obj)->window ? OBJECT(WIDGET(obj)->window)->name : "(null)",
	    AG_GetTicks());
#endif
	if ((win = WIDGET(obj)->window) != NULL) {
		AG_OBJECT_ISA(win, "AG_Widget:AG_Window:*");
		win->dirty = 1;
	}
}

/*
 * Alternate interface to AG_MapCursor(). Create a new mapping or
 * update the rectangle of an existing one.
 */
void
AG_SetCursor(void *obj, AG_CursorArea **ca, const AG_Rect *_Nonnull r,
    struct ag_cursor *c)
{
	AG_OBJECT_ISA(obj, "AG_Widget:*");

	if (*ca == NULL) {
		*ca = AG_MapCursor(obj, r, c);
	} else {
		(*ca)->r = *r;
	}
}

/*
 * Select one of the standard cursors. If the given cursor doesn't exist,
 * return NULL into *ca without any error message.
 */
void
AG_SetStockCursor(void *obj, AG_CursorArea **ca, const AG_Rect *r, int cName)
{
	AG_OBJECT_ISA(obj, "AG_Widget:*");

	if (*ca == NULL) {
		*ca = AG_MapStockCursor(obj, r, cName);
	} else {
		(*ca)->r = *r;
	}
}

/*
 * Process synchronous window operations. This includes focus changes,
 * visibility changes and the detach operation. Called from custom event
 * loops or driver code, after all queued events have been processed.
 */
void
AG_WindowProcessQueued(void)
{
	AG_LockVFS(&agDrivers);
	if (agWindowToFocus != NULL) { AG_WindowProcessFocusChange(); }
	if (!AG_TAILQ_EMPTY(&agWindowShowQ)) { AG_WindowProcessShowQueue(); }
	if (!AG_TAILQ_EMPTY(&agWindowHideQ)) { AG_WindowProcessHideQueue(); }
	if (!AG_TAILQ_EMPTY(&agWindowDetachQ)) { AG_WindowProcessDetachQueue(); }
	AG_UnlockVFS(&agDrivers);
}

#ifdef AG_LEGACY
void
AG_ViewAttach(struct ag_window *pWin)
{
	AG_ObjectAttach(agDriverSw, pWin);
}
void
AG_ViewDetach(AG_Window *win)
{
	AG_ObjectDetach(win);
}
void
AG_WindowSetPadding(AG_Window *win, int lPad, int rPad, int tPad, int bPad)
{
	AG_SetStyleF(win, "padding", "%d %d %d %d",
	    (tPad != -1) ? tPad : 0,
	    (rPad != -1) ? rPad : 0,
	    (bPad != -1) ? bPad : 0,
	    (lPad != -1) ? lPad : 0);
}
void
AG_WindowSetSpacing(AG_Window *win, int spacing)
{
	AG_SetStyleF(win, "spacing", "%d", spacing);
}
void
AG_WindowSetVisibility(AG_Window *win, int flag)
{
	AG_ObjectLock(win);
	if (win->visible) {
		AG_WindowHide(win);
	} else {
		AG_WindowShow(win);
	}
	AG_ObjectUnlock(win);
}
int
AG_WindowIntersect(AG_DriverSw *drv, int x, int y)
{
	AG_Window *win;
	int rv = 0;

	AG_FOREACH_WINDOW(win, drv) {
		if (win->visible &&
		    AG_WidgetArea(win, x, y))
			rv++;
	}
	return (rv);
}
#endif /* AG_LEGACY */

static void
Init(void *_Nonnull obj)
{
	AG_Window *win = obj;
	int i;
#ifdef AG_DEBUG
	int debugLvlSave;
#endif

	win->flags = AG_WINDOW_NOCURSORCHG;
#ifdef AG_DEBUG
	memset(win->caption, 0, sizeof(win->caption));
#else
	win->caption[0] = '\0';
#endif
	win->visible = 0;
	win->dirty = 0;
	win->alignment = AG_WINDOW_ALIGNMENT_NONE;
	win->tbar = NULL;
	win->icon = NULL;
	win->wMin = 0;
	win->hMin = 0;
	win->wBorderBot = agWindowBotBorderDefault;
	win->wBorderSide = agWindowSideBorderDefault;
	win->wResizeCtrl = 16;
	win->r.x = 0;
	win->r.y = 0;
	win->r.w = 0;
	win->r.h = 0;
	win->rSaved.x = -1;
	win->rSaved.y = -1;
	win->rSaved.w = -1;
	win->rSaved.h = -1;
	win->minPct = 50;
	win->nFocused = 0;
	win->widExclMotion = NULL;
	win->wmType = AG_WINDOW_WM_NORMAL;
	win->zoom = AG_ZOOM_DEFAULT;
	win->parent = NULL;
	win->transientFor = NULL;
	win->pinnedTo = NULL;

	TAILQ_INIT(&win->pvt.subwins);
	win->pvt.fade = NULL;

	TAILQ_INIT(&win->pvt.cursorAreas);
	for (i = 0; i < 5; i++)
		win->pvt.caResize[i] = NULL;

	AG_SetEvent(win, "window-gainfocus", OnGainFocus, NULL);
	AG_SetEvent(win, "window-lostfocus", OnLostFocus, NULL);

	/*
	 * We wish to forward incoming `widget-shown', `widget-hidden' and
	 * `detached' events to all attached child widgets.
	 */
	AG_SetEvent(win, "widget-shown", OnShow, NULL);
	AG_SetEvent(win, "widget-hidden", OnHide, NULL);
	AG_SetEvent(win, "detached", OnDetach, NULL);

	Debug_Mute(debugLvlSave);

	/* Use custom attach/detach hooks to keep the window stack in order. */
	AG_SetFn(win, "attach-fn", Attach, NULL);
	AG_SetFn(win, "detach-fn", Detach, NULL);
	
	/* Set the inheritable style defaults. */
	AG_SetString(win,  "font-family", OBJECT(agDefaultFont)->name);
	AG_SetStringF(win, "font-size",   "%.02fpt", agDefaultFont->spec.size);
	AG_SetString(win,  "font-weight", "normal");
	AG_SetString(win,  "font-style",  "normal");
	
	Debug_Unmute(debugLvlSave);

	WIDGET(win)->font = agDefaultFont;
	WIDGET(win)->pal = agDefaultPalette;
}

#if defined(AG_WIDGETS) && defined(AG_DEBUG)
static void
WindowCaptionChanged(AG_Event *event)
{
	char caption[AG_WINDOW_CAPTION_MAX];
	AG_Textbox *tb = AG_TEXTBOX_SELF();
	AG_Window *win = AG_WINDOW_PTR(1);

	AG_TextboxCopyString(tb, caption, sizeof(caption));
	AG_WindowSetCaptionS(win, caption);
}

static void
PollCursorAreas(AG_Event *event)
{
	AG_Tlist *tl = AG_TLIST_SELF();
	AG_Window *tgt = (AG_Window *)AG_PTR(1);
	AG_CursorArea *ca;
	
	AG_TlistBegin(tl);
	
	if (tgt == NULL || !AG_OBJECT_VALID(tgt) || !AG_WINDOW_ISA(tgt)) {
		AG_TlistAdd(tl, NULL, _("(Object %p is invalid)"), tgt);
		goto out;
	}

	TAILQ_FOREACH(ca, &tgt->pvt.cursorAreas, cursorAreas) {
		AG_TlistItem *it;

		it = AG_TlistAdd(tl, NULL,
		    _(AGSI_BOLD "%dx%d" AGSI_RST " rectangle at "
		      "[" AGSI_BOLD "%d,%d" AGSI_RST "] "
		      "-> Cursor " AGSI_YEL "%p%s" AGSI_RST
		      " (Widget " AGSI_YEL "%p" AGSI_RST ")"),
		    ca->r.w,
		    ca->r.h,
		    ca->r.x,
		    ca->r.y,
		    ca->c,
		    (!ca->stock) ? "[*]" : "",
		    ca->wid);
		it->p1 = ca;
	}
out:
	AG_TlistEnd(tl);
}

static void *_Nullable
Edit(void *_Nonnull obj)
{
	static const AG_FlagDescr flagDescr[] = {
	    { AG_WINDOW_MODAL,        N_("Application-modal (" AGSI_CODE "MODAL" AGSI_RST ")"), 0 },
	    { AG_WINDOW_MAXIMIZED,    N_("Maximized (" AGSI_CODE "MAXIMIZED" AGSI_RST ")"), 0 },
	    { AG_WINDOW_MINIMIZED,    N_("Minimized (" AGSI_CODE "MINIMIZED" AGSI_RST ")"), 0 },
	    { AG_WINDOW_KEEPABOVE,    N_("Keep above others (" AGSI_CODE "KEEPABOVE" AGSI_RST ")"), 0 },
	    { AG_WINDOW_KEEPBELOW,    N_("Keep below others (" AGSI_CODE "KEEPBELOW" AGSI_RST ")"), 0 },
	    { AG_WINDOW_DENYFOCUS,    N_("Deny focus (" AGSI_CODE "DENYFOCUS" AGSI_RST ")"), 1 },
	    { AG_WINDOW_NOHRESIZE,    N_("No horizontal resize (" AGSI_CODE "NOHRESIZE" AGSI_RST ")"), 1 },
	    { AG_WINDOW_NOVRESIZE,    N_("No vertical resize (" AGSI_CODE "NOVRESIZE" AGSI_RST ")"), 1 },
	    { AG_WINDOW_NOBACKGROUND, N_("Disable background (" AGSI_CODE "NOBACKGROUND" AGSI_RST ")"), 1 },
	    { AG_WINDOW_MAIN,         N_("Main window (" AGSI_CODE "MAIN" AGSI_RST ")"), 1 },
	    { AG_WINDOW_NOMOVE,       N_("Unmoveable (" AGSI_CODE "NOMOVE" AGSI_RST ")"), 1 },
	    { AG_WINDOW_NOCURSORCHG,  N_("Inhibit cursor changes (" AGSI_CODE "NOCURSORCHG" AGSI_RST ")"), 1 },
	    { AG_WINDOW_USE_TEXT,     N_("Using the font engine (" AGSI_CODE "USE_TEXT" AGSI_RST ")"), 0 },
	    { 0, NULL, 0 }
	};
	AG_Window *tgt = obj;
	AG_Box *box;
	AG_Textbox *tb;
	AG_Label *lbl;
	AG_Notebook *nb;
	AG_NotebookTab *nt;

	box = AG_BoxNewVert(NULL, AG_BOX_EXPAND);
	AG_SetPadding(box, "2 3 2 3");

	lbl = AG_LabelNew(box, 0, _("Window Structure " AGSI_YEL "%s" AGSI_RST),
	    OBJECT(tgt)->name);
	AG_SetFontSize(lbl, "130%");

	AG_LabelNewPolled(box, AG_LABEL_SLOW | AG_LABEL_HFILL,
	    _("Visible=" AGSI_BOLD "%i" AGSI_RST ", "
	      "Zoom=" AGSI_BOLD "%i" AGSI_RST ", "
	      "Position=[" AGSI_BOLD "%i,%i" AGSI_RST "], "
	      "Size=(" AGSI_BOLD "%i,%i" AGSI_RST "), "
	      "Min=(" AGSI_BOLD "%i,%i" AGSI_RST ")."),
	    &tgt->visible, &tgt->zoom,
	    &WIDGET(tgt)->x,
	    &WIDGET(tgt)->y,
	    &tgt->r.w,
	    &tgt->r.h,
	    &tgt->wMin,
	    &tgt->hMin);

	nb = AG_NotebookNew(box, AG_NOTEBOOK_EXPAND);

	nt = AG_NotebookAdd(nb, _("Flags"), AG_BOX_VERT);
	{
		AG_CheckboxSetFromFlags(nt, 0, &tgt->flags, flagDescr);
	}

	nt = AG_NotebookAdd(nb, _("Properties"), AG_BOX_VERT);
	AG_SetPadding(nt, "3");
	{
		AG_SpacerNewHoriz(nt);

		tb = AG_TextboxNewS(nt, 0, _("Caption: "));
		AG_TextboxSizeHint(tb, "<XXXXXXXXXXXXXXXXXXXXXXXXXXXX>");
		AG_TextboxBindUTF8(tb, tgt->caption, sizeof(tgt->caption));
		AG_SetEvent(tb, "textbox-postchg", WindowCaptionChanged, "%p", tgt);

		AG_SpacerNewHoriz(nt);

		if (tgt->flags & AG_WINDOW_MINSIZEPCT) {
			AG_NumericalNewInt(nt, 0, "%", _("Minimum Size: "),
			    &tgt->minPct);
		} else {
			AG_NumericalNewInt(nt, 0, "px", _("Minimum Width: "), &tgt->wMin);
			AG_NumericalNewInt(nt, 0, "px", _("Minimum Height: "), &tgt->hMin);
		}
		AG_NumericalNewInt(nt, 0, "px", _("Bottom Border Width: "), &tgt->wBorderBot);
		AG_NumericalNewInt(nt, 0, "px", _("Side Border Width: "), &tgt->wBorderSide);
		AG_NumericalNewInt(nt, 0, "px", _("Resize Control Width: "), &tgt->wResizeCtrl);
#if 0
		AG_SpacerNewHoriz(nt);
		AG_LabelNewS(nt, 0, _("Initial window position:"));
		AG_RadioNewUint(nt, 0, agWindowAlignmentNames, &tgt->alignment);
# ifdef AG_WM_HINTS
		AG_LabelNewS(nt, 0, _("EWMH Window Type:"));
		AG_RadioNewUint(nt, 0, agWindowWmTypeNames, &tgt->wmType);
# endif
#endif
		AG_SpacerNewHoriz(nt);

		AG_LabelNewPolledMT(nt, AG_LABEL_SLOW | AG_LABEL_HFILL,
		    &OBJECT(tgt)->lock,
		    _("Parent: " AGSI_YEL "%[objName]" AGSI_RST
		      " @ (" AGSI_CYAN "AG_Window" AGSI_RST " *)%p"),
		    &tgt->parent, &tgt->parent);

		AG_LabelNewPolledMT(nt, AG_LABEL_SLOW | AG_LABEL_HFILL,
		    &OBJECT(tgt)->lock,
		    _("Transient for: " AGSI_YEL "%[objName]" AGSI_RST
		      " @ (" AGSI_CYAN "AG_Window" AGSI_RST " *)%p"),
		    &tgt->transientFor, &tgt->transientFor);

		AG_LabelNewPolledMT(nt, AG_LABEL_SLOW | AG_LABEL_HFILL,
	            &OBJECT(tgt)->lock,
	            _("Pinned to: " AGSI_YEL "%[objName]" AGSI_RST
		      " @ (" AGSI_CYAN "AG_Window" AGSI_RST " *)%p"),
	            &tgt->pinnedTo, &tgt->pinnedTo);
	}

	nt = AG_NotebookAdd(nb, _("Cursor Areas"), AG_BOX_VERT);
	{
		AG_TlistNewPolledMs(nt, AG_TLIST_EXPAND, 125,
		    PollCursorAreas, "%p", tgt);
	}

	return (box);
}
#endif /* AG_WIDGETS and AG_DEBUG */

AG_WidgetClass agWindowClass = {
	{
		"Agar(Widget:Window)",
		sizeof(AG_Window),
		{ 1,0, AGC_WINDOW, 0xE024 },
		Init,
		NULL,		/* reset */
		NULL,		/* destroy */
		NULL,		/* load */
		NULL,		/* save */
#if defined(AG_WIDGETS) && defined(AG_DEBUG)
		Edit
#else
		NULL		/* edit */
#endif
	},
	Draw,
	SizeRequest,
	SizeAllocate,
	NULL,			/* mouse_button_down */
	NULL,			/* mouse_button_up */
	NULL,			/* mouse_motion */
	NULL,			/* key_down */
	NULL,			/* key_up */
	NULL,			/* touch */
	NULL,			/* ctrl */
	NULL			/* joy */
};
