Bug 732052 - Allow slide frames to use touch events. r=smaug,enn
authorWes Johnston <wjohnston@mozilla.com>
Mon, 09 Jul 2012 09:55:35 -0700
changeset 101828 207c9718fe3f2957866e81baa89895e46acb3f3d
parent 101827 363077c10ba97eef283a2d9cb5fb198f654b96f5
child 101829 db08a9bc1557f8be833ae9b0e7873035ab512e93
push idunknown
push userunknown
push dateunknown
reviewerssmaug, enn
bugs732052
milestone16.0a1
Bug 732052 - Allow slide frames to use touch events. r=smaug,enn
layout/base/nsPresShell.cpp
layout/generic/nsFrame.cpp
layout/xul/base/src/nsSliderFrame.cpp
layout/xul/base/src/nsSliderFrame.h
toolkit/content/tests/chrome/Makefile.in
toolkit/content/tests/chrome/test_scaledrag.xul
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -5896,16 +5896,57 @@ PresShell::HandleEvent(nsIFrame        *
       }
 
       return NS_OK;
     }
 
     PresShell* shell =
         static_cast<PresShell*>(frame->PresContext()->PresShell());
 
+    switch (aEvent->message) {
+      case NS_TOUCH_MOVE:
+      case NS_TOUCH_CANCEL:
+      case NS_TOUCH_END: {
+        // Remove the changed touches
+        // need to make sure we only remove touches that are ending here
+        nsTouchEvent* touchEvent = static_cast<nsTouchEvent*>(aEvent);
+        nsTArray<nsCOMPtr<nsIDOMTouch> >  &touches = touchEvent->touches;
+        for (PRUint32 i = 0; i < touches.Length(); ++i) {
+          nsIDOMTouch *touch = touches[i];
+          if (!touch) {
+            break;
+          }
+  
+          PRInt32 id;
+          touch->GetIdentifier(&id);
+          nsCOMPtr<nsIDOMTouch> oldTouch;
+          gCaptureTouchList.Get(id, getter_AddRefs(oldTouch));
+          if (!oldTouch) {
+            break;
+          }
+  
+          nsCOMPtr<nsPIDOMEventTarget> targetPtr;
+          oldTouch->GetTarget(getter_AddRefs(targetPtr));
+          nsCOMPtr<nsIContent> content = do_QueryInterface(targetPtr);
+          if (!content) {
+            break;
+          }
+
+          nsIFrame* contentFrame = content->GetPrimaryFrame();
+          if (!contentFrame) {
+            break;
+          }
+
+          shell = static_cast<PresShell*>(
+                      contentFrame->PresContext()->PresShell());
+        }
+        break;
+      }
+    }
+
     // Check if we have an active EventStateManager which isn't the
     // EventStateManager of the current PresContext.
     // If that is the case, and mouse is over some ancestor document,
     // forward event handling to the active document.
     // This way content can get mouse events even when
     // mouse is over the chrome or outside the window.
     //
     // Note, currently for backwards compatibility we don't forward mouse events
@@ -6472,27 +6513,45 @@ PresShell::DispatchTouchEvent(nsEvent *a
         continue;
       }
 
       nsTouchEvent newEvent(NS_IS_TRUSTED_EVENT(touchEvent) ?
                               true : false,
                             touchEvent);
       newEvent.target = targetPtr;
 
-      nsCOMPtr<nsIContent> content(do_QueryInterface(targetPtr));
+      // If someone is capturing, all touch events are filtered to their target
+      nsCOMPtr<nsIContent> content = GetCapturingContent();
+
+      // if no one is capturing, set the capturing target
+      if (!content) {
+        content = do_QueryInterface(targetPtr);
+      }
+      PresShell* contentPresShell = nsnull;
+      if (content && content->OwnerDoc() == mDocument) {
+        contentPresShell = static_cast<PresShell*>
+            (content->OwnerDoc()->GetShell());
+        if (contentPresShell) {
+          contentPresShell->PushCurrentEventInfo(
+              content->GetPrimaryFrame(), content);
+        }
+      }
       nsPresContext *context = nsContentUtils::GetContextForContent(content);
       if (!context) {
         context = mPresContext;
       }
       tmpStatus = nsEventStatus_eIgnore;
       nsEventDispatcher::Dispatch(targetPtr, context,
                                   &newEvent, nsnull, &tmpStatus, aEventCB);
       if (nsEventStatus_eConsumeNoDefault == tmpStatus) {
         preventDefault = true;
       }
+      if (contentPresShell) {
+        contentPresShell->PopCurrentEventInfo();
+      }
     }
   } else {
     // touchevents need to have the target attribute set on each touch
     nsTArray<nsCOMPtr<nsIDOMTouch> >  touches = touchEvent->touches;
     for (PRUint32 i = 0; i < touches.Length(); ++i) {
       nsIDOMTouch *touch = touches[i];
       if (touch->mChanged) {
         touch->SetTarget(mCurrentEventContent);
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -2252,25 +2252,26 @@ nsFrame::FireDOMEvent(const nsAString& a
 }
 
 NS_IMETHODIMP
 nsFrame::HandleEvent(nsPresContext* aPresContext, 
                      nsGUIEvent*     aEvent,
                      nsEventStatus*  aEventStatus)
 {
 
-  if (aEvent->message == NS_MOUSE_MOVE) {
+  if (aEvent->message == NS_MOUSE_MOVE || aEvent->message == NS_TOUCH_MOVE) {
     return HandleDrag(aPresContext, aEvent, aEventStatus);
   }
 
-  if (aEvent->eventStructType == NS_MOUSE_EVENT &&
-      static_cast<nsMouseEvent*>(aEvent)->button == nsMouseEvent::eLeftButton) {
-    if (aEvent->message == NS_MOUSE_BUTTON_DOWN) {
+  if ((aEvent->eventStructType == NS_MOUSE_EVENT &&
+      static_cast<nsMouseEvent*>(aEvent)->button == nsMouseEvent::eLeftButton) ||
+      aEvent->eventStructType == NS_TOUCH_EVENT) {
+    if (aEvent->message == NS_MOUSE_BUTTON_DOWN || aEvent->message == NS_TOUCH_START) {
       HandlePress(aPresContext, aEvent, aEventStatus);
-    } else if (aEvent->message == NS_MOUSE_BUTTON_UP) {
+    } else if (aEvent->message == NS_MOUSE_BUTTON_UP || aEvent->message == NS_TOUCH_END) {
       HandleRelease(aPresContext, aEvent, aEventStatus);
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsFrame::GetDataForTableSelection(const nsFrameSelection *aFrameSelection,
--- a/layout/xul/base/src/nsSliderFrame.cpp
+++ b/layout/xul/base/src/nsSliderFrame.cpp
@@ -15,16 +15,17 @@
 #include "nsPresContext.h"
 #include "nsIContent.h"
 #include "nsCOMPtr.h"
 #include "nsINameSpaceManager.h"
 #include "nsGkAtoms.h"
 #include "nsHTMLParts.h"
 #include "nsIPresShell.h"
 #include "nsCSSRendering.h"
+#include "nsDOMTouchEvent.h"
 #include "nsEventListenerManager.h"
 #include "nsIDOMEventTarget.h"
 #include "nsIDOMMouseEvent.h"
 #include "nsScrollbarButtonFrame.h"
 #include "nsISliderListener.h"
 #include "nsIScrollbarMediator.h"
 #include "nsScrollbarFrame.h"
 #include "nsRepeatService.h"
@@ -432,19 +433,22 @@ nsSliderFrame::HandleEvent(nsPresContext
   nsIBox* scrollbarBox = GetScrollbar();
   nsCOMPtr<nsIContent> scrollbar;
   scrollbar = GetContentOfBox(scrollbarBox);
   bool isHorizontal = IsHorizontal();
 
   if (isDraggingThumb())
   {
     switch (aEvent->message) {
+    case NS_TOUCH_MOVE:
     case NS_MOUSE_MOVE: {
-      nsPoint eventPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent,
-                                                                         this);
+      nsPoint eventPoint;
+      if (!GetEventPoint(aEvent, eventPoint)) {
+        break;
+      }
       if (mChange) {
         // We're in the process of moving the thumb to the mouse,
         // but the mouse just moved.  Make sure to update our
         // destination point.
         mDestinationPoint = eventPoint;
         StopRepeat();
         StartRepeat();
         break;
@@ -474,29 +478,34 @@ nsSliderFrame::HandleEvent(nsPresContext
         else {
           // vertical scrollbar - check if mouse is left or right of thumb
           if (eventPoint.x < -gSnapMultiplier * thumbSize.width ||
               eventPoint.x > thumbSize.width +
                                gSnapMultiplier * thumbSize.width)
             isMouseOutsideThumb = true;
         }
       }
+      if (aEvent->eventStructType == NS_TOUCH_EVENT) {
+        *aEventStatus = nsEventStatus_eConsumeNoDefault;
+      }
       if (isMouseOutsideThumb)
       {
         SetCurrentThumbPosition(scrollbar, mThumbStart, false, true, false);
         return NS_OK;
       }
 
       // set it
       SetCurrentThumbPosition(scrollbar, pos, false, true, true); // with snapping
     }
     break;
 
+    case NS_TOUCH_END:
     case NS_MOUSE_BUTTON_UP:
-      if (static_cast<nsMouseEvent*>(aEvent)->button == nsMouseEvent::eLeftButton ||
+      if (aEvent->message == NS_TOUCH_END ||
+          static_cast<nsMouseEvent*>(aEvent)->button == nsMouseEvent::eLeftButton ||
           (static_cast<nsMouseEvent*>(aEvent)->button == nsMouseEvent::eMiddleButton &&
            gMiddlePref)) {
         // stop capturing
         AddListener();
         DragThumb(false);
         if (mChange) {
           StopRepeat();
           mChange = 0;
@@ -514,20 +523,23 @@ nsSliderFrame::HandleEvent(nsPresContext
 #ifdef XP_MACOSX
               // On Mac the option key inverts the scroll-to-here preference.
               (static_cast<nsMouseEvent*>(aEvent)->IsAlt() != GetScrollToClick())) ||
 #else
               (static_cast<nsMouseEvent*>(aEvent)->IsShift() != GetScrollToClick())) ||
 #endif
              (gMiddlePref && aEvent->message == NS_MOUSE_BUTTON_DOWN &&
               static_cast<nsMouseEvent*>(aEvent)->button ==
-                nsMouseEvent::eMiddleButton)) {
+                nsMouseEvent::eMiddleButton) ||
+             (aEvent->message == NS_TOUCH_START && GetScrollToClick())) {
 
-    nsPoint eventPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent,
-                                                                      this);
+    nsPoint eventPoint;
+    if (!GetEventPoint(aEvent, eventPoint)) {
+      return NS_OK;
+    }
     nscoord pos = isHorizontal ? eventPoint.x : eventPoint.y;
 
     // adjust so that the middle of the thumb is placed under the click
     nsIFrame* thumbFrame = mFrames.FirstChild();
     if (!thumbFrame) {
       return NS_OK;
     }
     nsSize thumbSize = thumbFrame->GetSize();
@@ -535,16 +547,19 @@ nsSliderFrame::HandleEvent(nsPresContext
 
     // set it
     nsWeakFrame weakFrame(this);
     // should aMaySnap be true here?
     SetCurrentThumbPosition(scrollbar, pos - thumbLength/2, false, false, false);
     NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_OK);
 
     DragThumb(true);
+    if (aEvent->eventStructType == NS_TOUCH_EVENT) {
+      *aEventStatus = nsEventStatus_eConsumeNoDefault;
+    }
 
     if (isHorizontal)
       mThumbStart = thumbFrame->GetPosition().x;
     else
       mThumbStart = thumbFrame->GetPosition().y;
 
     mDragStart = pos - mThumbStart;
   }
@@ -554,16 +569,50 @@ nsSliderFrame::HandleEvent(nsPresContext
   //   HandleRelease(aPresContext, aEvent, aEventStatus);
 
   if (aEvent->message == NS_MOUSE_EXIT_SYNTH && mChange)
      HandleRelease(aPresContext, aEvent, aEventStatus);
 
   return nsFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
 }
 
+bool
+nsSliderFrame::GetEventPoint(nsGUIEvent* aEvent, nsPoint &aPoint) {
+  nsIntPoint refPoint;
+  nsresult rv;
+  if (aEvent->eventStructType == NS_TOUCH_EVENT) {
+    rv = GetTouchPoint(static_cast<nsTouchEvent*>(aEvent), refPoint);
+    if (NS_FAILED(rv))
+       return false;
+  } else {
+    refPoint = aEvent->refPoint;
+  }
+  aPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, refPoint, this);
+  return true;
+}
+
+bool
+nsSliderFrame::GetTouchPoint(nsTouchEvent* aEvent, nsIntPoint &aPoint)
+{
+  NS_ENSURE_ARG_POINTER(aEvent);
+  // return false if there is more than one touch on the page, or if
+  // we can't find a touch point
+  if (aEvent->touches.Length() != 1) {
+    return false;
+  }
+
+  nsIDOMTouch *touch = aEvent->touches.SafeElementAt(0);
+  if (!touch) {
+    return false;
+  }
+  nsDOMTouch* domtouch = static_cast<nsDOMTouch*>(touch);
+  aPoint = domtouch->mRefPoint;
+  return true;
+}
+
 // Helper function to collect the "scroll to click" metric. Beware of
 // caching this, users expect to be able to change the system preference
 // and see the browser change its behavior immediately.
 bool
 nsSliderFrame::GetScrollToClick()
 {
   // if there is no parent scrollbar, check the movetoclick attribute. If set
   // to true, always scroll to the click point. If false, never do this.
@@ -819,54 +868,57 @@ nsSliderFrame::SetInitialChildList(Child
   return r;
 }
 
 nsresult
 nsSliderMediator::HandleEvent(nsIDOMEvent* aEvent)
 {
   // Only process the event if the thumb is not being dragged.
   if (mSlider && !mSlider->isDraggingThumb())
-    return mSlider->MouseDown(aEvent);
+    return mSlider->StartDrag(aEvent);
 
   return NS_OK;
 }
 
 nsresult
-nsSliderFrame::MouseDown(nsIDOMEvent* aMouseEvent)
+nsSliderFrame::StartDrag(nsIDOMEvent* aEvent)
 {
 #ifdef DEBUG_SLIDER
   printf("Begin dragging\n");
 #endif
 
-  nsCOMPtr<nsIDOMMouseEvent> mouseEvent(do_QueryInterface(aMouseEvent));
-  if (!mouseEvent)
-    return NS_OK;
-
   if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
                             nsGkAtoms::_true, eCaseMatters))
     return NS_OK;
 
-  PRUint16 button = 0;
-  mouseEvent->GetButton(&button);
-  if (!(button == 0 || (button == 1 && gMiddlePref)))
-    return NS_OK;
+  bool isHorizontal = IsHorizontal();
+  bool scrollToClick = false;
 
-  bool isHorizontal = IsHorizontal();
-
-  bool scrollToClick = false;
+  nsCOMPtr<nsIDOMMouseEvent> mouseEvent(do_QueryInterface(aEvent));
+  if (mouseEvent) {
+    PRUint16 button = 0;
+    mouseEvent->GetButton(&button);
+    if (!(button == 0 || (button == 1 && gMiddlePref)))
+      return NS_OK;
+  
 #ifndef XP_MACOSX
-  // On Mac there's no scroll-to-here when clicking the thumb
-  mouseEvent->GetShiftKey(&scrollToClick);
-  if (button != 0) {
-    scrollToClick = true;
+    // On Mac there's no scroll-to-here when clicking the thumb
+    mouseEvent->GetShiftKey(&scrollToClick);
+    if (button != 0) {
+      scrollToClick = true;
+    }
+#endif
   }
-#endif
+
+  nsGUIEvent *event = static_cast<nsGUIEvent*>(aEvent->GetInternalNSEvent());
 
-  nsPoint pt =  nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(mouseEvent,
-                                                                this);
+  nsPoint pt;
+  if (!GetEventPoint(event, pt)) {
+    return NS_OK;
+  }
   nscoord pos = isHorizontal ? pt.x : pt.y;
 
   // If shift click or middle button, first
   // place the middle of the slider thumb under the click
   nsCOMPtr<nsIContent> scrollbar;
   nscoord newpos = pos;
   if (scrollToClick) {
     // adjust so that the middle of the thumb is placed under the click
@@ -941,16 +993,19 @@ nsSliderFrame::AddListener()
 
   nsIFrame* thumbFrame = mFrames.FirstChild();
   if (!thumbFrame) {
     return;
   }
   thumbFrame->GetContent()->
     AddSystemEventListener(NS_LITERAL_STRING("mousedown"), mMediator,
                            false, false);
+  thumbFrame->GetContent()->
+    AddSystemEventListener(NS_LITERAL_STRING("touchstart"), mMediator,
+                           false, false);
 }
 
 void
 nsSliderFrame::RemoveListener()
 {
   NS_ASSERTION(mMediator, "No listener was ever added!!");
 
   nsIFrame* thumbFrame = mFrames.FirstChild();
@@ -961,37 +1016,47 @@ nsSliderFrame::RemoveListener()
     RemoveSystemEventListener(NS_LITERAL_STRING("mousedown"), mMediator, false);
 }
 
 NS_IMETHODIMP
 nsSliderFrame::HandlePress(nsPresContext* aPresContext,
                            nsGUIEvent*     aEvent,
                            nsEventStatus*  aEventStatus)
 {
+  if (aEvent->message == NS_TOUCH_START && GetScrollToClick()) {
+    printf("Bailing for touch\n");
+    return NS_OK;
+  }
+
+  if (aEvent->message == NS_MOUSE_BUTTON_DOWN) {
 #ifdef XP_MACOSX
-  // On Mac the option key inverts the scroll-to-here preference.
-  if (((nsMouseEvent *)aEvent)->IsAlt() != GetScrollToClick())
+    // On Mac the option key inverts the scroll-to-here preference.
+    if (((nsMouseEvent *)aEvent)->IsAlt() != GetScrollToClick()) {
 #else
-  if (((nsMouseEvent *)aEvent)->IsShift() != GetScrollToClick())
+    if (((nsMouseEvent *)aEvent)->IsShift() != GetScrollToClick()) {
 #endif
-    return NS_OK;
+      return NS_OK;
+    }
+  }
 
   nsIFrame* thumbFrame = mFrames.FirstChild();
   if (!thumbFrame) // display:none?
     return NS_OK;
 
   if (mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
                             nsGkAtoms::_true, eCaseMatters))
     return NS_OK;
   
   nsRect thumbRect = thumbFrame->GetRect();
   
   nscoord change = 1;
-  nsPoint eventPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent,
-                                                                    this);
+  nsPoint eventPoint;
+  if (!GetEventPoint(aEvent, eventPoint)) {
+    return NS_OK;
+  }
   if (IsHorizontal() ? eventPoint.x < thumbRect.x 
                      : eventPoint.y < thumbRect.y)
     change = -1;
 
   mChange = change;
   DragThumb(true);
   mDestinationPoint = eventPoint;
   StartRepeat();
--- a/layout/xul/base/src/nsSliderFrame.h
+++ b/layout/xul/base/src/nsSliderFrame.h
@@ -92,17 +92,17 @@ public:
                          nsGUIEvent* aEvent,
                          nsEventStatus* aEventStatus);
 
   NS_IMETHOD SetInitialChildList(ChildListID     aListID,
                                  nsFrameList&    aChildList);
 
   virtual nsIAtom* GetType() const;
 
-  nsresult MouseDown(nsIDOMEvent* aMouseEvent);
+  nsresult StartDrag(nsIDOMEvent* aEvent);
 
   static PRInt32 GetCurrentPosition(nsIContent* content);
   static PRInt32 GetMinPosition(nsIContent* content);
   static PRInt32 GetMaxPosition(nsIContent* content);
   static PRInt32 GetIncrement(nsIContent* content);
   static PRInt32 GetPageIncrement(nsIContent* content);
   static PRInt32 GetIntegerAttribute(nsIContent* content, nsIAtom* atom, PRInt32 defaultValue);
   void EnsureOrient();
@@ -133,16 +133,26 @@ private:
   void SetCurrentThumbPosition(nsIContent* aScrollbar, nscoord aNewPos, bool aIsSmooth,
                                bool aImmediateRedraw, bool aMaySnap);
   void SetCurrentPosition(nsIContent* aScrollbar, PRInt32 aNewPos, bool aIsSmooth,
                           bool aImmediateRedraw);
   void SetCurrentPositionInternal(nsIContent* aScrollbar, PRInt32 pos,
                                   bool aIsSmooth, bool aImmediateRedraw);
   nsresult CurrentPositionChanged(nsPresContext* aPresContext,
                                   bool aImmediateRedraw);
+
+  // Get the point associated with this event. Returns true if a valid point
+  // was found. Otherwise false.
+  bool GetEventPoint(nsGUIEvent *aEvent, nsPoint &aPoint);
+
+  // Get the point associated with this touch event. Returns true if a valid point
+  // was found. False if there is more than one touch present on the page, or
+  // if a point could not be found for the given touch.
+  bool GetTouchPoint(nsTouchEvent *aEvent, nsIntPoint &aPoint);
+
   void DragThumb(bool aGrabMouseEvents);
   void AddListener();
   void RemoveListener();
   bool isDraggingThumb();
 
   void StartRepeat() {
     nsRepeatService::GetInstance()->Start(Notify, this);
   }
--- a/toolkit/content/tests/chrome/Makefile.in
+++ b/toolkit/content/tests/chrome/Makefile.in
@@ -95,16 +95,17 @@ MOCHITEST_CHROME_FILES = 	findbar_window
 		test_menulist.xul \
 		test_menuitem_blink.xul \
 		test_menulist_keynav.xul \
 		test_popup_coords.xul \
 		test_popup_recreate.xul \
 		test_popup_preventdefault.xul \
 		test_notificationbox.xul \
 		test_scale.xul \
+		test_scaledrag.xul \
 		test_radio.xul \
 		test_tabbox.xul \
 		test_progressmeter.xul \
 		test_props.xul \
 		test_statusbar.xul \
 		test_timepicker.xul \
 		test_tree.xul \
 		test_tree_view.xul \
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_scaledrag.xul
@@ -0,0 +1,197 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+XUL <scale> dragging tests
+-->
+<window title="Dragging XUL scale tests" width="500" height="600"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>  
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>  
+
+  <hbox flex="1">
+    <scale id="scale1" orient="horizontal" flex="1" min="0" max="4" value="2"/>
+    <scale id="scale2" orient="vertical" flex="1" min="0" max="4" value="2"/>
+    <scale id="scale3" orient="horizontal" flex="1" movetoclick="true" min="0" max="4" value="2"/>
+  </hbox>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script>
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+function getThumb(aScale) {
+  return document.getAnonymousElementByAttribute(aScale, "class", "scale-thumb");
+}
+
+function sendTouch(aType, aRect, aDX, aDY, aMods) {
+  var cwu = SpecialPowers.getDOMWindowUtils(window);
+  var x = aRect.left + aRect.width/2  + aDX;
+  var y = aRect.top  + aRect.height/2 + aDY;
+  if (/mouse/.test(aType))
+    cwu.sendMouseEvent(aType, x, y, 0, 1, aMods || 0, false);
+  else
+    cwu.sendTouchEvent(aType, [0], [x], [y], [1], [1], [0], [1], 1, aMods || 0, true);
+}
+
+function getOffset(aScale, aDir) {
+  var rect = aScale.getBoundingClientRect();
+  var d = aScale.orient == "horizontal" ? rect.width/4 : rect.height/4;
+  switch (aDir) {
+    case "right":      return [   d,   0];
+    case "left":       return [-1*d,   0];
+    case "up":         return [   0,-1*d];
+    case "down":       return [   0,   d];
+    case "downleft":   return [ -1*d,   d];
+    case "upleft":     return [ -1*d,-1*d];
+    case "downright": return [d,   d];
+    case "upright":   return [d,-1*d];
+  }
+  return [0,0];
+}
+
+function testTouchDragThumb(aDesc, aId, aDir, aVal1, aVal2, aMods) {
+  info(aDesc);
+  var scale = document.getElementById(aId);
+  var [x,y] = getOffset(scale, aDir);
+
+  sendTouch("touchstart", getThumb(scale).getBoundingClientRect(), 0, 0, aMods);
+  is(scale.value, aVal1, "Touchstart on thumb has correct value");
+  sendTouch("touchmove", getThumb(scale).getBoundingClientRect(), x, y, aMods);
+  sendTouch("touchend", getThumb(scale).getBoundingClientRect(), 0, 0, aMods);
+  is(scale.value, aVal2, "After touch " + (aDir ? ("and drag " + aDir + " ") : "") + "on thumb, scale has correct value");
+
+  scale.value = 2;
+}
+
+function testMouseDragThumb(aDesc, aId, aDir, aVal1, aVal2, aMods) {
+  info(aDesc);
+  var scale = document.getElementById(aId);
+  var [x,y] = getOffset(scale, aDir);
+
+  sendTouch("mousedown", getThumb(scale).getBoundingClientRect(), 0, 0, aMods);
+  is(scale.value, aVal1, "Mousedown on thumb has correct value");
+  sendTouch("mousemove", getThumb(scale).getBoundingClientRect(), x, y, aMods);
+  sendTouch("mouseup", getThumb(scale).getBoundingClientRect(), 0, 0, aMods);
+  is(scale.value, aVal2, "After mouseup " + (aDir ? ("and drag " + aDir + " ") : "") + "on thumb, scale has correct value");
+
+  scale.value = 2;
+}
+
+function testTouchDragSlider(aDesc, aId, aDir, aVal1, aVal2, aMods) {
+  info(aDesc);
+  var scale = document.getElementById(aId);
+  var [x,y] = getOffset(scale, aDir);
+
+  sendTouch("touchstart", getThumb(scale).getBoundingClientRect(), x, y, aMods);
+  is(scale.value, aVal1, "Touchstart on slider has correct value");
+  sendTouch("touchmove", getThumb(scale).getBoundingClientRect(), -x, -y, aMods);
+  sendTouch("touchend", getThumb(scale).getBoundingClientRect(), 0, 0, aMods);
+  is(scale.value, aVal2, "After touch " + (aDir ? ("and drag " + aDir + " ") : "") + "on slider, scale has correct value");
+
+  scale.value = 2;
+}
+
+function testMouseDragSlider(aDesc, aId, aDir, aVal1, aVal2, aMods) {
+  info(aDesc);
+  var scale = document.getElementById(aId);
+  var [x,y] = getOffset(scale, aDir);
+
+  sendTouch("mousedown", getThumb(scale).getBoundingClientRect(), x, y, aMods);
+  is(scale.value, aVal1, "Mousedown on slider has correct value");
+  sendTouch("mousemove", getThumb(scale).getBoundingClientRect(), -x, -y, aMods);
+  sendTouch("mouseup", getThumb(scale).getBoundingClientRect(), 0, 0, aMods);
+  is(scale.value, aVal2, "After mouseup " + (aDir ? ("and drag " + aDir + " ") : "") + "on slider, scale has correct value");
+
+  scale.value = 2;
+}
+
+function runTests() {
+  // test dragging a horizontal slider with touch events by tapping on the thumb
+  testTouchDragThumb("Touch Horizontal Thumb", "scale1", "", 2, 2);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Left", "scale1", "left", 2, 1);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Right", "scale1", "right", 2, 3);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Up", "scale1", "up", 2, 2);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Down", "scale1", "down", 2, 2);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Downleft", "scale1", "downleft", 2, 1);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Upleft", "scale1", "upleft", 2, 1);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Upright", "scale1", "upright", 2, 3);
+  testTouchDragThumb("TouchDrag Horizontal Thumb Downright", "scale1", "downright", 2, 3);
+
+  // test dragging a horizontal slider with mouse events by clicking on the thumb
+  testMouseDragThumb("Click Horizontal Thumb", "scale1", "", 2, 2);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Left", "scale1", "left", 2, 1);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Right", "scale1", "right", 2, 3);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Up", "scale1", "up", 2, 2);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Down", "scale1", "down", 2, 2);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Downleft", "scale1", "downleft", 2, 1);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Upleft", "scale1", "upleft", 2, 1);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Upright", "scale1", "upright", 2, 3);
+  testMouseDragThumb("MouseDrag Horizontal Thumb Downright", "scale1", "downright", 2, 3);
+
+  // test dragging a vertical slider with touch events by tapping on the thumb
+  testTouchDragThumb("Touch Vertical Thumb", "scale2", "", 2, 2);
+  testTouchDragThumb("TouchDrag Vertical Thumb Left", "scale2", "left", 2, 2);
+  testTouchDragThumb("TouchDrag Vertical Thumb Right", "scale2", "right", 2, 2);
+  testTouchDragThumb("TouchDrag Vertical Thumb Up", "scale2", "up", 2, 1);
+  testTouchDragThumb("TouchDrag Vertical Thumb Down", "scale2", "down", 2, 3);
+  testTouchDragThumb("TouchDrag Vertical Thumb Downleft", "scale2", "downleft", 2, 3);
+  testTouchDragThumb("TouchDrag Vertical Thumb Upleft", "scale2", "upleft", 2, 1);
+  testTouchDragThumb("TouchDrag Vertical Thumb Upright", "scale2", "upright", 2, 1);
+  testTouchDragThumb("TouchDrag Vertical Thumb Downright", "scale2", "downright", 2, 3);
+
+  // test dragging a vertical slider with mouse events by clicking on the thumb
+  testMouseDragThumb("Click Vertical Thumb", "scale2", "", 2, 2);
+  testMouseDragThumb("MouseDrag Vertical Thumb Left", "scale2", "left", 2, 2);
+  testMouseDragThumb("MouseDrag Vertical Thumb Right", "scale2", "right", 2, 2);
+  testMouseDragThumb("MouseDrag Vertical Thumb Up", "scale2", "up", 2, 1);
+  testMouseDragThumb("MouseDrag Vertical Thumb Down", "scale2", "down", 2, 3);
+  testMouseDragThumb("MouseDrag Vertical Thumb Downleft", "scale2", "downleft", 2, 3);
+  testMouseDragThumb("MouseDrag Vertical Thumb Upleft", "scale2", "upleft", 2, 1);
+  testMouseDragThumb("MouseDrag Vertical Thumb Upright", "scale2", "upright", 2, 1);
+  testMouseDragThumb("MouseDrag Vertical Thumb Downright", "scale2", "downright", 2, 3);
+
+  var isMac = /Mac/.test(navigator.platform);
+
+  // test dragging a slider by tapping off the thumb
+  testTouchDragSlider("TouchDrag Slider Left", "scale1", "left", isMac ? 1 : 0, isMac ? 2 : 0);
+  testTouchDragSlider("TouchDrag Slider Right", "scale1", "right", isMac ? 3 : 4, isMac ? 2 : 4);
+  testMouseDragSlider("MouseDrag Slider Left", "scale1", "left", isMac ? 1 : 0, isMac ? 2 : 0);
+  testMouseDragSlider("MouseDrag Slider Right", "scale1", "right", isMac ? 3 : 4, isMac ? 2 : 4);
+
+  // test dragging a slider by tapping off the thumb and holding shift
+  // modifiers don't affect touch events
+  var mods = /Mac/.test(navigator.platform) ? Components.interfaces.nsIDOMNSEvent.ALT_MASK :
+                                              Components.interfaces.nsIDOMNSEvent.SHIFT_MASK;
+  testTouchDragSlider("TouchDrag Slider Left+Shift", "scale1", "left", isMac ? 1 : 0, isMac ? 2 : 0, mods);
+  testTouchDragSlider("TouchDrag Slider Right+Shift", "scale1", "right", isMac ? 3 : 4, isMac ? 2 : 4, mods);
+  testMouseDragSlider("MouseDrag Slider Left+Shift", "scale1", "left", isMac ? 0 : 1, isMac ? 0 : 2, mods);
+  testMouseDragSlider("MouseDrag Slider Right+Shift", "scale1", "right", isMac ? 4 : 3, isMac ? 4 : 2, mods);
+
+  // test dragging a slider with movetoclick="true" by tapping off the thumb
+  testTouchDragSlider("TouchDrag Slider Left+MoveToClick", "scale3", "left", 1, 2);
+  testTouchDragSlider("TouchDrag Slider Right+MoveToClick", "scale3", "right", 3, 2);
+  testMouseDragSlider("MouseDrag Slider Left+MoveToClick", "scale3", "left", 1, 2);
+  testMouseDragSlider("MouseDrag Slider Right+MoveToClick", "scale3", "right", 3, 2);
+
+  // test dragging a slider by tapping off the thumb and holding shift
+  // modifiers don't affect touch events
+  testTouchDragSlider("MouseDrag Slider Left+MoveToClick+Shift", "scale3", "left", 1, 2, mods);
+  testTouchDragSlider("MouseDrag Slider Right+MoveToClick+Shift", "scale3", "right", 3, 2, mods);
+  testMouseDragSlider("MouseDrag Slider Left+MoveToClick+Shift", "scale3", "left", 0, 0, mods);
+  testMouseDragSlider("MouseDrag Slider Right+MoveToClick+Shift", "scale3", "right", 4, 4, mods);
+
+  SimpleTest.finish();
+}
+
+addLoadEvent(function() { SimpleTest.executeSoon(runTests); });
+]]></script>
+
+</window>