diff --git a/src/as/com/threerings/flex/ChatDisplayBox.as b/src/as/com/threerings/flex/ChatDisplayBox.as
new file mode 100644
index 00000000..92a1f020
--- /dev/null
+++ b/src/as/com/threerings/flex/ChatDisplayBox.as
@@ -0,0 +1,82 @@
+package com.threerings.flex {
+
+import flash.display.DisplayObjectContainer;
+
+import flash.events.Event;
+
+import mx.controls.TextArea;
+
+import com.threerings.crowd.chat.client.ChatDirector;
+import com.threerings.crowd.chat.client.ChatDisplay;
+import com.threerings.crowd.chat.data.ChatMessage;
+import com.threerings.crowd.chat.data.UserMessage;
+import com.threerings.crowd.util.CrowdContext;
+
+/**
+ * A very simple chat display.
+ */
+public class ChatDisplayBox extends TextArea
+ implements ChatDisplay
+{
+ public function ChatDisplayBox (ctx :CrowdContext)
+ {
+ _ctx = ctx;
+ this.editable = false;
+
+ // TODO
+ width = 400;
+ height = 150;
+ }
+
+ // documentation inherited from interface ChatDisplay
+ public function clear () :void
+ {
+ this.htmlText = "";
+ }
+
+ // documentation inherited from interface ChatDisplay
+ public function displayMessage (
+ msg :ChatMessage, alreadyDisplayed :Boolean) :Boolean
+ {
+ if (!_scrollBot) {
+ _scrollBot = (verticalScrollPosition == maxVerticalScrollPosition);
+ }
+
+ // display the message
+ if (msg is UserMessage) {
+ this.htmlText += "<" +
+ (msg as UserMessage).speaker + "> ";
+ }
+ this.htmlText += msg.message;
+ return true;
+ }
+
+ override public function parentChanged (p :DisplayObjectContainer) :void
+ {
+ super.parentChanged(p);
+
+ var chatdir :ChatDirector = _ctx.getChatDirector();
+ if (p != null) {
+ chatdir.addChatDisplay(this);
+ } else {
+ chatdir.removeChatDisplay(this);
+ }
+ }
+
+ // documentation inherited
+ override protected function updateDisplayList (uw :Number, uh :Number) :void
+ {
+ super.updateDisplayList(uw, uh);
+
+ if (_scrollBot) {
+ verticalScrollPosition = maxVerticalScrollPosition;
+ _scrollBot = false;
+ }
+ }
+
+ /** The giver of life. */
+ protected var _ctx :CrowdContext;
+
+ protected var _scrollBot :Boolean;
+}
+}
diff --git a/src/as/com/threerings/flex/CommandButton.as b/src/as/com/threerings/flex/CommandButton.as
new file mode 100644
index 00000000..373620b4
--- /dev/null
+++ b/src/as/com/threerings/flex/CommandButton.as
@@ -0,0 +1,54 @@
+package com.threerings.flex {
+
+import flash.events.MouseEvent;
+
+import mx.controls.Button;
+
+/**
+ * A command button simply dispatches a Controller command (with an optional
+ * argument) when it is clicked.
+ */
+public class CommandButton extends Button
+{
+ /**
+ * Create a command button.
+ */
+ public function CommandButton (cmd :String = null, arg :Object = null)
+ {
+ setCommand(cmd, arg);
+ }
+
+ /**
+ * Set the command and argument to be issued when this button is pressed.
+ */
+ public function setCommand (cmd :String, arg :Object = null) :void
+ {
+ _cmd = cmd;
+ _arg = arg;
+ }
+
+ override protected function clickHandler (event :MouseEvent) :void
+ {
+ super.clickHandler(event);
+
+ if (enabled) {
+ // stop the click event
+ event.stopImmediatePropagation();
+
+ // dispatch the command event
+ // if the arg is null and we're a toggle button, dispatch our state
+ var arg :Object = _arg;
+ if (arg == null && toggle) {
+ arg = selected;
+ }
+ CommandEvent.dispatch(this, _cmd, arg);
+ }
+ }
+
+ /** The command to submit when clicked. */
+ protected var _cmd :String;
+
+ /** The argument that accompanies our command. */
+ protected var _arg :Object;
+}
+}
diff --git a/src/as/com/threerings/flex/CommandEvent.as b/src/as/com/threerings/flex/CommandEvent.as
new file mode 100644
index 00000000..e00b723a
--- /dev/null
+++ b/src/as/com/threerings/flex/CommandEvent.as
@@ -0,0 +1,88 @@
+package com.threerings.flex {
+
+import flash.errors.IllegalOperationError;
+
+import flash.events.Event;
+import flash.events.IEventDispatcher;
+
+public class CommandEvent extends Event
+{
+ /** The event type for all controller events. */
+ public static const TYPE :String = "commandEvt";
+
+ /**
+ * Use this method to dispatch CommandEvents.
+ */
+ public static function dispatch (
+ disp :IEventDispatcher, cmd :String, arg :Object = null) :void
+ {
+ // Create the event
+ var event :CommandEvent = create(cmd, arg);
+
+ // Dispatch it. A return value of true means that the event was
+ // never cancelled, so we complain.
+ if (disp == null || disp.dispatchEvent(event)) {
+ Log.getLog(CommandEvent).warning("Unhandled controller command " +
+ "[cmd=" + cmd + ", arg=" + arg + "].");
+ }
+ }
+
+ /** The command. */
+ public var command :String;
+
+ /** An optional argument. */
+ public var arg :Object;
+
+ /**
+ * Command events may not be directly constructed, use the dispatch
+ * method to do your work.
+ */
+ public function CommandEvent (command :String, arg :Object)
+ {
+ super(TYPE, true, true);
+ if (_blockConstructor) {
+ throw new IllegalOperationError();
+ }
+ this.command = command;
+ this.arg = arg;
+ }
+
+ /**
+ * Mark this command as handled, stopping its propagation up the
+ * hierarchy.
+ */
+ public function markAsHandled () :void
+ {
+ preventDefault();
+ stopImmediatePropagation();
+ }
+
+ override public function clone () :Event
+ {
+ return create(command, arg);
+ }
+
+ override public function toString () :String
+ {
+ return "CommandEvent[" + command + " (" + arg + ")]";
+ }
+
+ /**
+ * A factory method for privately creating command events.
+ */
+ protected static function create (cmd :String, arg :Object) :CommandEvent
+ {
+ var event :CommandEvent;
+ _blockConstructor = false;
+ try {
+ event = new CommandEvent(cmd, arg);
+ } finally {
+ _blockConstructor = true;
+ }
+ return event;
+ }
+
+ /** Used to prevent unauthorized construction. */
+ protected static var _blockConstructor :Boolean = true;
+}
+}
diff --git a/src/as/com/threerings/flex/CommandMenu.as b/src/as/com/threerings/flex/CommandMenu.as
new file mode 100644
index 00000000..4aadeb48
--- /dev/null
+++ b/src/as/com/threerings/flex/CommandMenu.as
@@ -0,0 +1,176 @@
+package com.threerings.flex {
+
+import flash.display.DisplayObject;
+import flash.display.DisplayObjectContainer;
+
+import flash.geom.Rectangle;
+
+import mx.controls.Menu;
+import mx.core.mx_internal;
+import mx.core.ScrollPolicy;
+import mx.events.MenuEvent;
+
+/**
+ * A pretty standard menu that can submit CommandEvents if menu items
+ * have "command" and possibly "arg" properties. Commands are submitted to
+ * controllers for processing. Alternatively, you may specify
+ * "callback" properties that specify a function closure to call, with the
+ * "arg" property containing either a single arg or an array of args.
+ *
+ * Example dataProvider array:
+ * [ { label: "Go home", icon: homeIconClass,
+ * command: Controller.GO_HOME, arg: homeId },
+ * { type: "separator"},
+ { label: "Crazytown", callback: setCrazy, arg: [ true, false ] },
+ * { label: "Other places", children: subMenuArray }
+ * ];
+ *
+ * See "Defining menu structure and data" in the Flex manual for the
+ * full list.
+ */
+public class CommandMenu extends Menu
+{
+ public function CommandMenu ()
+ {
+ super();
+
+ addEventListener(MenuEvent.ITEM_CLICK, itemClicked);
+// addEventListener(MenuEvent.MENU_SHOW, handleMenuShown);
+ }
+
+ /**
+ * Factory method to create a command menu.
+ *
+ * @param parent The parent of this menu.
+ * @param items an array of menu items.
+ */
+ public static function createMenu (
+ parent :DisplayObjectContainer, items :Array) :CommandMenu
+ {
+ var menu :CommandMenu = new CommandMenu();
+ menu.tabEnabled = false;
+ menu.showRoot = true;
+ Menu.popUpMenu(menu, parent, items);
+ return menu;
+ }
+
+ /**
+ * Actually pop up the menu. This can be used instead of show().
+ */
+ public function popUp (
+ trigger :DisplayObject, popUpwards :Boolean = false) :void
+ {
+ var r :Rectangle = trigger.getBounds(trigger.stage);
+
+ if (popUpwards) {
+ show(r.x, 0);
+ // then, reposition the y once we know our size
+ y = r.y - getExplicitOrMeasuredHeight();
+
+ } else {
+ // simply position it below the trigger
+ show(r.x, r.y + r.height);
+ }
+ }
+
+ /**
+ * Callback for MenuEvent.ITEM_CLICK.
+ */
+ protected function itemClicked (event :MenuEvent) :void
+ {
+ var arg :Object = getItemArgument(event.item);
+ var cmd :String = getItemCommand(event.item);
+ var fn :Function;
+ if (cmd == null) {
+ fn = getItemCallback(event.item);
+ }
+ if (cmd != null || fn != null) {
+ event.stopImmediatePropagation();
+ if (cmd != null) {
+ CommandEvent.dispatch(mx_internal::parentDisplayObject, cmd, arg);
+
+ } else {
+ try {
+ var args :Array = (arg as Array);
+ if (args == null && arg != null) {
+ args = [ arg ];
+ }
+ fn.apply(null, args);
+
+ } catch (err :Error) {
+ Log.getLog(this).warning("Unable to call menu callback: " +
+ event.item);
+ }
+ }
+ }
+ }
+
+// /**
+// * Callback for MenuEvent.MENU_SHOW.
+// */
+// protected function handleMenuShown (event :MenuEvent) :void
+// {
+// // just ensure that every menu can scroll
+// event.menu.verticalScrollPolicy = ScrollPolicy.ON;
+// }
+
+ /**
+ * Get the command for the specified item, if any.
+ * Somewhat similar to bits in the DefaultDataDescriptor.
+ */
+ protected function getItemCommand (item :Object) :String
+ {
+ try {
+ if (item is XML) {
+ return (item.@command as String);
+
+ } else if (item is Object) {
+ return (item.command as String);
+ }
+ } catch (e :Error) {
+ // fall through
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the callback function for the specified item, if any.
+ */
+ protected function getItemCallback (item :Object) :Function
+ {
+ try {
+ if (item is XML) {
+ return (item.@callback as Function);
+
+ } else if (item is Object) {
+ return (item.callback as Function);
+ }
+ } catch (e :Error) {
+ // fall through
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the command for the specified item, if any.
+ * Somewhat similar to bits in the DefaultDataDescriptor.
+ */
+ protected function getItemArgument (item :Object) :Object
+ {
+ try {
+ if (item is XML) {
+ return item.@arg;
+
+ } else if (item is Object) {
+ return item.arg;
+ }
+ } catch (e :Error) {
+ // fall through
+ }
+
+ return null;
+ }
+}
+}
diff --git a/src/as/com/threerings/flex/CursorManager.as b/src/as/com/threerings/flex/CursorManager.as
new file mode 100644
index 00000000..2d1cfd9d
--- /dev/null
+++ b/src/as/com/threerings/flex/CursorManager.as
@@ -0,0 +1,301 @@
+package com.threerings.flex {
+
+import flash.display.DisplayObject;
+import flash.display.InteractiveObject;
+import flash.display.Sprite;
+import flash.events.MouseEvent;
+import flash.text.TextField;
+import flash.text.TextFieldType;
+import flash.ui.Mouse;
+
+import mx.core.Application;
+import mx.managers.SystemManager;
+import mx.styles.CSSStyleDeclaration;
+import mx.styles.StyleManager;
+
+import com.threerings.util.HashMap;
+
+/**
+ * A better CursorManager.
+ */
+public class CursorManager
+{
+ public static const SYSTEM_CURSOR :int = 0;
+
+ public static const BUSY_CURSOR :int = 1;
+
+ //--------------------------------------------------------------------------
+ //
+ // Class methods
+ //
+ //--------------------------------------------------------------------------
+
+ /**
+ * Makes the cursor visible.
+ * Cursor visibility is not reference-counted.
+ * A single call to the showCursor() method
+ * always shows the cursor regardless of how many calls
+ * to the hideCursor() method were made.
+ */
+ public static function showCursor () :void
+ {
+ if (_currentCursorId == SYSTEM_CURSOR) {
+ Mouse.show();
+ } else {
+ _cursorHolder.visible = true;
+ }
+ }
+
+ /**
+ * Makes the cursor invisible.
+ * Cursor visibility is not reference-counted.
+ * A single call to the hideCursor() method
+ * always hides the cursor regardless of how many calls
+ * to the showCursor() method were made.
+ */
+ public static function hideCursor () :void
+ {
+ _cursorHolder.visible = false;
+ Mouse.hide();
+ }
+
+ public static function getCurrentCursorId () :int
+ {
+ return _currentCursorId;
+ }
+
+ /**
+ * Creates a new cursor.
+ *
+ * @param cursorClass Class of the cursor to display.
+ *
+ * @param xOffset Number that specifies the x offset
+ * of the cursor, in pixels, relative to the mouse pointer.
+ * The default value is 0.
+ *
+ * @param yOffset Number that specifies the y offset
+ * of the cursor, in pixels, relative to the mouse pointer.
+ * The default value is 0.
+ *
+ * @return The ID of the cursor.
+ */
+ public static function addCursor (
+ cursorClass :Class, xOffset :int = 0, yOffset :int = 0) :int
+ {
+ var cursorId :int = _nextCursorId++;
+
+ var rec :CursorRecord = new CursorRecord(cursorClass);
+ rec.x = xOffset;
+ rec.y = yOffset;
+
+ _cursors.put(cursorId, rec);
+
+ return cursorId;
+ }
+
+ /**
+ * Set the current cursor.
+ */
+ public static function setCursor (id :int) :void
+ {
+ if (id == _currentCursorId) {
+ return;
+ }
+
+ if (!_initialized) {
+ // oh, let's set it up
+ _systemManager = Application.application.systemManager;
+
+ // set up the busy cursor
+ var cursorManagerStyleDeclaration :CSSStyleDeclaration =
+ StyleManager.getStyleDeclaration("CursorManager");
+ var busyCursorClass :Class =
+ cursorManagerStyleDeclaration.getStyle("busyCursor");
+ _busyCursor = new CursorRecord(busyCursorClass);
+
+ // The first time a cursor is requested of the CursorManager,
+ // create a Sprite to hold the cursor symbol
+ _cursorHolder = new Sprite();
+ _cursorHolder.mouseEnabled = false;
+ _systemManager.cursorChildren.addChild(_cursorHolder);
+
+ _initialized = true;
+ }
+
+ // figure out what the new cursor will be like
+ var rec :CursorRecord;
+ if (id == BUSY_CURSOR) {
+ rec = _busyCursor;
+
+ } else if (id != SYSTEM_CURSOR) {
+ rec = (_cursors.get(id) as CursorRecord);
+ if (rec == null) {
+ // a bogus id was specified
+ // TODO: throw an error?
+ return;
+ }
+ }
+
+ if (rec != null && (rec.cursor is Class)) {
+ // go ahead and instantiate the class
+ try {
+ var disp :DisplayObject = new (rec.cursor as Class);
+ rec.cursor = disp;
+ if (disp is InteractiveObject) {
+ (disp as InteractiveObject).mouseEnabled = false;
+ }
+
+ } catch (err :Error) {
+ // this cursor is not usable, bail
+ return;
+ }
+ }
+
+ // always remove any custom cursors from the hierarchy
+ if (_cursorHolder.numChildren > 0) {
+ _cursorHolder.removeChildAt(0);
+ }
+
+ if (id != SYSTEM_CURSOR) {
+ Mouse.hide();
+ var currentCursor :DisplayObject = (rec.cursor as DisplayObject);
+ _cursorHolder.addChild(currentCursor);
+ _cursorHolder.x = _systemManager.mouseX + rec.x;
+ _cursorHolder.y = _systemManager.mouseY + rec.y;
+
+ _currentXOffset = rec.x;
+ _currentYOffset = rec.y;
+ _systemManager.stage.addEventListener(
+ MouseEvent.MOUSE_MOVE, mouseMoveHandler);
+
+
+ } else {
+ _currentXOffset = 0;
+ _currentYOffset = 0;
+ _systemManager.stage.removeEventListener(
+ MouseEvent.MOUSE_MOVE, mouseMoveHandler);
+
+ Mouse.show();
+ }
+
+ _currentCursorId = id;
+ }
+
+ /**
+ * Removes a cursor from the cursor list.
+ * If the cursor being removed is the currently displayed cursor,
+ * the CursorManager displays the next cursor in the list, if one exists.
+ * If the list becomes empty, the CursorManager displays
+ * the default system cursor.
+ *
+ * @param cursorID ID of cursor to remove.
+ */
+ public static function removeCursor (id :int) :void
+ {
+ var rec :CursorRecord = (_cursors.remove(id) as CursorRecord);
+
+ if (rec != null && id == _currentCursorId) {
+ setCursor(SYSTEM_CURSOR);
+ }
+ }
+
+ /**
+ * Removes all of the cursors from the cursor list
+ * and restores the system cursor.
+ */
+ public static function removeAllCursors () :void
+ {
+ _cursors.clear();
+
+ if (_currentCursorId != BUSY_CURSOR) {
+ setCursor(SYSTEM_CURSOR);
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ //
+ // Class event handlers
+ //
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ private static function mouseMoveHandler (event :MouseEvent) :void
+ {
+ _cursorHolder.x = _systemManager.mouseX + _currentXOffset;
+ _cursorHolder.y = _systemManager.mouseY + _currentYOffset;
+
+ var target :Object = event.target;
+
+ // Do target test.
+ if (!_overTextField &&
+ target is TextField && target.type == TextFieldType.INPUT) {
+ _overTextField = true;
+ _showSystemCursor = true;
+
+ } else if (_overTextField &&
+ !(target is TextField && target.type == TextFieldType.INPUT)) {
+ _overTextField = false;
+ _showCustomCursor = true;
+ }
+
+ // Handle switching between system and custom cursor.
+ if (_showSystemCursor) {
+ _showSystemCursor = false;
+ _cursorHolder.visible = false;
+ Mouse.show();
+ }
+ if (_showCustomCursor) {
+ _showCustomCursor = false;
+ _cursorHolder.visible = true;
+ Mouse.hide();
+ }
+ }
+
+ /** A mapping of all assigned cursor ids. */
+ private static const _cursors :HashMap = new HashMap();
+
+ private static var _currentCursorId :int = 0; // SYSTEM_CURSOR
+
+ private static var _nextCursorId :int = 2; // skip BUSY
+
+ private static var _initialized :Boolean = false;
+
+ private static var _cursorHolder :Sprite;
+
+ private static var _currentXOffset :int = 0;
+ private static var _currentYOffset :int = 0;
+
+ /** A record for the busy cursor (where it can't be removed). */
+ private static var _busyCursor :CursorRecord;
+
+ private static var _overTextField :Boolean = false;
+
+ private static var _overLink :Boolean = false;
+
+ private static var _showSystemCursor :Boolean = false;
+
+ private static var _showCustomCursor :Boolean = false;
+
+ private static var _systemManager :SystemManager = null;
+}
+}
+
+
+/**
+ */
+class CursorRecord extends Object
+{
+ public function CursorRecord (clazz :Class)
+ {
+ cursor = clazz;
+ }
+
+ /** The class, or instantiated cursor. */
+ public var cursor :Object
+
+ /** The x/y offset for the hotspot. */
+ public var x :Number;
+ public var y :Number;
+}
diff --git a/src/as/com/threerings/flex/FunctionEffect.as b/src/as/com/threerings/flex/FunctionEffect.as
new file mode 100644
index 00000000..9e669252
--- /dev/null
+++ b/src/as/com/threerings/flex/FunctionEffect.as
@@ -0,0 +1,31 @@
+package com.threerings.flex {
+
+import mx.effects.IEffectInstance;
+import mx.effects.Effect;
+
+public class FunctionEffect extends Effect
+{
+ /** The function to call. */
+ public var func :Function;
+
+ /** The arguments to pass to the function. */
+ public var args :Array;
+
+ public function FunctionEffect (target :Object = null)
+ {
+ super(target);
+
+ instanceClass = FunctionEffectInstance;
+ }
+
+ // documentation inherited
+ override protected function initInstance (instance :IEffectInstance) :void
+ {
+ super.initInstance(instance);
+
+ var fe :FunctionEffectInstance = (instance as FunctionEffectInstance);
+ fe.func = func;
+ fe.args = args;
+ }
+}
+}
diff --git a/src/as/com/threerings/flex/FunctionEffectInstance.as b/src/as/com/threerings/flex/FunctionEffectInstance.as
new file mode 100644
index 00000000..88bef0df
--- /dev/null
+++ b/src/as/com/threerings/flex/FunctionEffectInstance.as
@@ -0,0 +1,29 @@
+package com.threerings.flex {
+
+import mx.effects.effectClasses.ActionEffectInstance;
+
+public class FunctionEffectInstance extends ActionEffectInstance
+{
+ public function FunctionEffectInstance (target :Object)
+ {
+ super(target);
+ }
+
+ /** The function to call. */
+ public var func :Function;
+
+ /** The args to pass. */
+ public var args :Array;
+
+ override public function play () :void
+ {
+ super.play();
+
+ // call the function!
+ func.apply(null, args);
+
+ // and we're done
+ finishRepeat();
+ }
+}
+}
diff --git a/src/as/com/threerings/flex/ScrollBox.as b/src/as/com/threerings/flex/ScrollBox.as
new file mode 100644
index 00000000..4a795fce
--- /dev/null
+++ b/src/as/com/threerings/flex/ScrollBox.as
@@ -0,0 +1,147 @@
+package com.threerings.flex {
+
+import flash.display.DisplayObject;
+import flash.display.Graphics;
+import flash.display.Sprite;
+
+import flash.events.Event;
+import flash.events.MouseEvent;
+
+import flash.geom.Matrix;
+import flash.geom.Rectangle;
+
+import mx.containers.Canvas;
+
+/**
+ * A control for scrolling some component separately.
+ */
+public class ScrollBox extends Canvas
+{
+ public function ScrollBox (
+ target :DisplayObject, maxWidth :int, maxHeight :int)
+ {
+ _target = target;
+ _maxWidth = maxWidth;
+ _maxHeight = maxHeight;
+
+ opaqueBackground = 0xFFFFFF;
+
+ _box = new Sprite();
+ rawChildren.addChild(_box);
+
+ recheckBounds();
+
+ // TODO: only when something changes
+ addEventListener(Event.ENTER_FRAME, enterFrame);
+
+ _box.addEventListener(MouseEvent.MOUSE_DOWN, spritePressed);
+ }
+
+ override public function setActualSize (w :Number, h :Number) :void
+ {
+ super.setActualSize(w, h);
+
+ recheckBounds();
+
+ graphics.clear();
+ graphics.lineStyle(1, 0);
+ graphics.drawRect(0, 0, width, height);
+ }
+
+ protected function enterFrame (vent :Event) :void
+ {
+ if (!_dragging) {
+ recheckBounds();
+ }
+ }
+
+ /**
+ * Get the bounds of the scrollable area. Broken-out for easy overridding.
+ */
+ protected function getScrollBounds () :Rectangle
+ {
+ var bounds :Rectangle = _target.transform.pixelBounds;
+// var m :Matrix = _target.transform.concatenatedMatrix;
+// bounds.topLeft = m.transformPoint(bounds.topLeft);
+// bounds.bottomRight = m.transformPoint(bounds.bottomRight);
+ return bounds;
+ }
+
+ protected function recheckBounds () :void
+ {
+ var bounds :Rectangle = getScrollBounds();
+
+ // see what's visible, what's not..
+ var scroller :Rectangle = _target.scrollRect;
+ if (scroller == null) {
+ scroller = bounds.clone();
+ }
+
+ if ((_bounds != null) && _bounds.equals(bounds) &&
+ (_scroller != null) && _scroller.equals(scroller)) {
+ return;
+ }
+
+ _bounds = bounds.clone();
+ _scroller = scroller;
+
+ _scale = Math.min(1,
+ Math.min(_maxWidth / _bounds.width, _maxHeight / _bounds.height));
+ width = _scale * _bounds.width;
+ height = _scale * _bounds.height;
+
+ drawBox(_box, _scroller.width * _scale, _scroller.height * _scale);
+
+ _box.x = (_scroller.x - _bounds.x) * _scale;
+ _box.y = (_scroller.y - _bounds.y) * _scale;
+ }
+
+ protected function drawBox (box :Sprite, ww :Number, hh :Number) :void
+ {
+ var g :Graphics = _box.graphics;
+ g.clear();
+ g.beginFill(0x0000FF);
+ g.drawRect(0, 0, ww, hh);
+ g.endFill();
+ }
+
+ protected function spritePressed (evt :MouseEvent) :void
+ {
+ _box.addEventListener(Event.ENTER_FRAME, boxUpdate);
+ _box.startDrag(true,
+ new Rectangle(0, 0, width - _box.width, height - _box.height));
+ stage.addEventListener(MouseEvent.MOUSE_UP, spriteReleased);
+ _dragging = true;
+ }
+
+ protected function spriteReleased (evt :MouseEvent) :void
+ {
+ stage.removeEventListener(MouseEvent.MOUSE_UP, spriteReleased);
+ _box.stopDrag();
+ _box.removeEventListener(Event.ENTER_FRAME, boxUpdate);
+ _dragging = false;
+ }
+
+ protected function boxUpdate (evt :Event) :void
+ {
+ var r :Rectangle = _target.scrollRect;
+ r.x = (_box.x / _scale) + _bounds.x;
+ r.y = (_box.y / _scale) + _bounds.y;
+ _target.scrollRect = r;
+ }
+
+ protected var _dragging :Boolean;
+
+ protected var _target :DisplayObject;
+
+ protected var _maxWidth :int;
+ protected var _maxHeight :int;
+
+ protected var _box :Sprite;
+
+ protected var _scale :Number;
+
+ protected var _bounds :Rectangle;
+ protected var _scroller :Rectangle;
+}
+}