/* fader.c - main code for fader custom control */

#include <windows.h>
#include "fader.h"

#define NO_DRAG              0    /* Values of FADER_THUMBSTATE */
#define DRAG                 1

#define FADER_THUMB_OFFSET   2    /* Distance in pixels from edge */

/* Window extra bytes */
#define FADER_RANGE          0    /* logical range values returned */
#define FADER_VALUE          4    /* Current logical value */
#define FADER_THUMBSTATE     6    /* DRAG or NO_DRAG */
#define FADER_WNDEXTRA       8    /* Total # of window extra bytes */

HANDLE hGlobFaderInstance = NULL;
char szGlobControlName[] = "Fader";

/* Forward declarations for completeness */

static BOOL NEAR PASCAL RegisterControlClass (HANDLE hInstance);
LONG FAR PASCAL FaderWndFn (HWND hWnd, WORD wMsg,
            WORD wParam, LONG lParam);
int GetThumbHeight(int iTotalHeight);
void GetFaderThumbRect(HWND hWnd, LPRECT pRc, LPRECT pThumbRect,
            int iCurPos);
int  XlatPosPhysicalToLogical(LONG lLogRange, int iPhysMax,
            int iPhysMin, int iCurPos);
int  XlatPosLogicalToPhysical(LONG lLogRange, int iPhysMax,
            int iPhysMin, int iLogPos);
static void DrawCaret(HDC hDC, LPRECT lprc);
static void PaintFader(HWND hWnd);

/* These are DLL initialization and control functions */

BOOL FAR PASCAL LibMain (HANDLE hModule, WORD wDataSeg,
   WORD wHeapSize, LPSTR lpszCmdLine)
{
   hGlobFaderInstance = hModule;
   if (wHeapSize != 0)  /* Moveable DS */
      UnlockData(0);
   return RegisterControlClass(hModule);
}


int FAR PASCAL WEP (int nSystemExit)
{
   UnregisterClass(szGlobControlName, hGlobFaderInstance);
   return 1;                 /* never fails */
}


/* This function can be static if you only plan to use it
   in a DLL */

BOOL NEAR PASCAL RegisterControlClass (HANDLE hInstance)
{
   WNDCLASS wc;

   wc.style         = CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW;
   wc.lpfnWndProc   = FaderWndFn;
   wc.cbClsExtra    = 0;
   wc.cbWndExtra    = FADER_WNDEXTRA;
   wc.hInstance     = hInstance;
   wc.hIcon         = NULL;
   wc.hCursor       = LoadCursor(NULL, IDC_SIZENS);
   wc.hbrBackground = COLOR_WINDOW + 1;
   wc.lpszMenuName  = NULL;
   wc.lpszClassName = szGlobControlName;
   return RegisterClass(&wc);
}


/* This function is handy for sending the FDRN_... messages
   back to the parent.  Note that FDRN_THUMBTRACK is disabled
   unless FDRS_TRACK is enabled */

static LONG NEAR PASCAL NotifyParent (HWND hWnd,
   WORD wNotifyCode)
{
   BOOL bSend=TRUE;

   if (wNotifyCode == FDRN_THUMBTRACK)
      bSend = (BOOL) (GetWindowLong(hWnd, GWL_STYLE) & FDRS_TRACK);

   if (bSend)
      return SendMessage(GetParent(hWnd), WM_COMMAND,
             GetWindowWord(hWnd, GWW_ID),
             MAKELONG(hWnd, wNotifyCode));
   else
      return 0;
}


/* This is the main "window function" procedure, it is called
   sometimes by the SDK Dialog box editor */

LONG FAR PASCAL FaderWndFn (HWND hWnd, WORD wMsg,
   WORD wParam, LONG lParam)
{
   LONG lResult = 0;
   HDC hDC;
   POINT pt;
   RECT rc;
   int iLogPos;
   int iOldLogPos;
   int iPhyPos;
   int iMaxLog;
   int iMinLog;
   RECT thumb_rect;
   LONG lLogRange;
   int iThumbHalf;
   HANDLE hNewBrush, hOldBrush;

   switch (wMsg) {
      case WM_CREATE:
         SendMessage(hWnd, FDRM_SETRANGE, 0, MAKELONG(0, 100));
         SendMessage(hWnd, FDRM_SETLOGVALUE, 0, 0);
         break;

      case WM_GETDLGCODE: /* interface query by dialog manager */
         lResult = DLGC_WANTARROWS;
         break;

      case WM_PAINT:
         PaintFader(hWnd);
         break;

      case WM_SETFOCUS: /* receiving the keyboard focus */
      case WM_KILLFOCUS: /* losing the keyboard focus */
            /* calculate update region */
            GetClientRect(hWnd, &rc);
            iLogPos = GetWindowWord(hWnd, FADER_VALUE);
            GetFaderThumbRect(hWnd, (LPRECT)&rc, (LPRECT)&thumb_rect,
               iLogPos);
            if (thumb_rect.left == rc.left)
               break;

            /* force a repaint */
            hDC = GetDC( hWnd );
            if (hDC) {
               /* define appropriate brush & text colors */
               if (hNewBrush = (HBRUSH)SendMessage( GetParent(hWnd),
                  WM_CTLCOLOR, hDC, MAKELONG(hWnd,CTLCOLOR_BTN) ) )
                  hOldBrush = SelectObject(hDC,hNewBrush);
               else
                  hOldBrush = NULL;

               /* draw caret */
               DrawCaret(hDC, (LPRECT)&thumb_rect);

               /* restore original brush */
               if ( hNewBrush ) {
                  SelectObject( hDC, hOldBrush );
                  DeleteObject( hNewBrush );
                  }

               /* release display context */
               ReleaseDC( hWnd, hDC );
               }
         break;

      case WM_KEYDOWN:    /* process virtual key code */
         GetClientRect(hWnd, &rc);
         iLogPos = (int)GetWindowWord(hWnd, FADER_VALUE);
         iOldLogPos = iLogPos;
         lLogRange = GetWindowLong(hWnd, FADER_RANGE);
         iMaxLog = HIWORD(lLogRange);
         iMinLog = LOWORD(lLogRange);

         switch (wParam) {
            case VK_HOME : /* home key */
               iLogPos = iMinLog;
               break;

            case VK_END : /* end key */
               iLogPos = iMaxLog;
               break;

            case VK_LEFT : /* cursor left key */
            case VK_DOWN : /* cursor down key */
               if (iLogPos < iMaxLog)
                  iLogPos++;
               break;

            case VK_UP : /* cursor up key */
            case VK_RIGHT : /* cursor right key */
               if (iLogPos > iMinLog)
                  iLogPos--;
               break;

            case VK_PRIOR : /* page up key */
               iLogPos -= (iMaxLog - iMinLog) / 8;
               if (iLogPos < iMinLog)
                  iLogPos = iMinLog;
               break;

            case VK_NEXT : /* page down key */
               iLogPos += (iMaxLog - iMinLog) / 8;
               if (iLogPos > iMaxLog)
                  iLogPos = iMaxLog;
               break;

            default : /* something else */
               break;
            }

         if (iLogPos != iOldLogPos) /* did it change? */
            {
            SendMessage(hWnd, FDRM_SETLOGVALUE, iLogPos, 0);
            NotifyParent(hWnd, FDRN_THUMBTRACK);

            /* Invalidate old position */
            GetFaderThumbRect(hWnd, (LPRECT)&rc,
                 (LPRECT)&thumb_rect, iOldLogPos);
            InvalidateRect(hWnd, (LPRECT)&thumb_rect, TRUE);

            /* Invalidate new position */
            GetFaderThumbRect(hWnd, (LPRECT)&rc,
                (LPRECT)&thumb_rect, iLogPos);
            InvalidateRect(hWnd, (LPRECT)&thumb_rect, TRUE);
            }
         break;

      case WM_LBUTTONDOWN:
         GetClientRect(hWnd, &rc);
         iLogPos = GetWindowWord(hWnd, FADER_VALUE);
         GetFaderThumbRect(hWnd, (LPRECT)&rc, (LPRECT)&thumb_rect, iLogPos);
         pt = MAKEPOINT(lParam);
         /* is the mouse in the "hot" rectangle? */
         if ((thumb_rect.top <= pt.y) && (thumb_rect.bottom >= pt.y) &&
            (thumb_rect.left <= pt.x) && (thumb_rect.right  >= pt.x)) {
            SetWindowWord(hWnd, FADER_THUMBSTATE, DRAG);
            hDC = GetDC(hWnd);
            InvertRect(hDC, (LPRECT)&thumb_rect);
            ReleaseDC(hWnd, hDC);

            NotifyParent(hWnd, FDRN_THUMBTRACK);
            /* grab focus if necessary */
            if ( GetFocus() != hWnd )
                SetFocus( hWnd );
            /* Lock mouse on to this window */
            SetCapture(hWnd);
            }
         break;

      case WM_MOUSEMOVE:
      case WM_LBUTTONUP:
         /* Nothing to do if not in drag! */
         if ((wMsg == WM_MOUSEMOVE) &&
             (GetWindowWord(hWnd, FADER_THUMBSTATE) != DRAG) ) {
            lResult = DefWindowProc(hWnd, wMsg, wParam, lParam);
            break;
            }

         /* set the new current position and ask for redraw */
         pt = MAKEPOINT(lParam);
         GetClientRect(hWnd, &rc);
         iLogPos = GetWindowWord(hWnd, FADER_VALUE);
         lLogRange = GetWindowLong(hWnd, FADER_RANGE);

         /* Invalidate old position */
         GetFaderThumbRect(hWnd, (LPRECT)&rc, (LPRECT)&thumb_rect, iLogPos);
         InvalidateRect(hWnd, (LPRECT)&thumb_rect, TRUE);

         iPhyPos = pt.y;
         iThumbHalf = GetThumbHeight(rc.bottom)/2;
         if (iPhyPos < iThumbHalf)
            iPhyPos = iThumbHalf;
         if (iPhyPos > rc.bottom-iThumbHalf)
            iPhyPos = rc.bottom-iThumbHalf;

         iLogPos = XlatPosPhysicalToLogical(lLogRange,
            rc.bottom-iThumbHalf, rc.top+iThumbHalf, iPhyPos);
         GetFaderThumbRect(hWnd, (LPRECT)&rc, (LPRECT)&thumb_rect, iLogPos);
         InvalidateRect(hWnd, (LPRECT)&thumb_rect, TRUE);

         SetWindowWord(hWnd, FADER_VALUE, iLogPos);
         NotifyParent(hWnd, FDRN_THUMBTRACK);

         if (wMsg == WM_LBUTTONUP) {
            SetWindowWord(hWnd, FADER_THUMBSTATE, NO_DRAG);
            NotifyParent(hWnd, FDRN_ENDFADER);
            ReleaseCapture();
            }
         break;

      case FDRM_SETRANGE:
         GetClientRect(hWnd, &rc);
         InvalidateRect(hWnd, (LPRECT)&rc, TRUE);
         SetWindowLong(hWnd, FADER_RANGE, lParam);
         break;

      case FDRM_GETRANGE:
         lResult = GetWindowLong(hWnd, FADER_RANGE);
         break;

      case FDRM_SETLOGVALUE:
         GetClientRect(hWnd, &rc);
         InvalidateRect(hWnd, (LPRECT)&rc, TRUE);
         SetWindowWord(hWnd, FADER_VALUE, wParam);
         break;

      case FDRM_GETLOGVALUE:
         lResult = GetWindowWord(hWnd, FADER_VALUE);
         break;

      case FDRM_GETPHYSVALUE:
         GetClientRect(hWnd, &rc);
         iThumbHalf = GetThumbHeight(rc.bottom)/2;
         lLogRange = GetWindowLong(hWnd, FADER_RANGE);
         iLogPos = GetWindowWord(hWnd, FADER_VALUE);
         lResult = (long) XlatPosLogicalToPhysical(lLogRange,
            rc.bottom-iThumbHalf, rc.top+iThumbHalf, iLogPos);
         break;

      case FDRM_SETPHYSVALUE:
         GetClientRect(hWnd, &rc);
         iThumbHalf = GetThumbHeight(rc.bottom)/2;
         lLogRange = GetWindowLong(hWnd, FADER_RANGE);
         iLogPos = XlatPosPhysicalToLogical(lLogRange,
            rc.bottom-iThumbHalf, rc.top+iThumbHalf, wParam);
         SetWindowWord(hWnd, FADER_VALUE, iLogPos);
         InvalidateRect(hWnd, NULL, TRUE);
         break;

      default:
         lResult = DefWindowProc(hWnd, wMsg, wParam, lParam);
         break;
   }
   return(lResult);
}



/* This is a "helper" function which draws the entire fader */

static void PaintFader(HWND hWnd)
{
   PAINTSTRUCT ps;
   HANDLE  hOldPen;
   HANDLE hNewBrush, hOldBrush;
   HANDLE hMyParent;
   RECT thumb_rect;
   int x_center, y_coord;
   int iLogPos;
   int iPhyPos;
   int iThumbHalf;
   LONG lLogRange;
   HDC hDC;
   RECT rc;

   /* Default system color is COLOR_BTNFACE  from WNDCLASS structure */
   hDC = BeginPaint(hWnd, &ps);

   GetClientRect(hWnd, &rc);

   hOldPen = SelectObject( hDC,
       CreatePen(PS_SOLID,1,GetSysColor(COLOR_WINDOWFRAME)) );

   /* define appropriate brush & text colors */
   hMyParent = GetParent(hWnd);
   if (hNewBrush = (HBRUSH)SendMessage( hMyParent, WM_CTLCOLOR,
      ps.hdc, MAKELONG(hWnd,CTLCOLOR_FADER) ) )
      hOldBrush = SelectObject(ps.hdc,hNewBrush);
   else
      hOldBrush = NULL;

   /* Draws with horizontal symmetry */
   x_center = rc.right / 2;

   /* draw fader slot (clockwise) */
   MoveTo(hDC, x_center, 1);
   LineTo(hDC, x_center+1, 2);
   LineTo(hDC, x_center+1, rc.bottom-2);
   LineTo(hDC, x_center, rc.bottom-1);
   LineTo(hDC, x_center-1, rc.bottom-2);
   LineTo(hDC, x_center-1, 2);
   LineTo(hDC, x_center, 1);

   /* draw gridlines (top to bottom, left to right) */
   for (y_coord=3; y_coord < rc.bottom-3; y_coord+=3) {
      /* Left half first */
      MoveTo(hDC, 1, y_coord);
      LineTo(hDC, x_center-1, y_coord);
      /* Then right half */
      MoveTo(hDC, x_center+2, y_coord);
      LineTo(hDC, rc.right-1, y_coord);
      }

   /* draw filled box and then dividing line */
   iThumbHalf = GetThumbHeight(rc.bottom)/2;
   iLogPos = GetWindowWord(hWnd, FADER_VALUE);
   lLogRange = GetWindowLong(hWnd, FADER_RANGE);
   iPhyPos =  XlatPosLogicalToPhysical(lLogRange, rc.bottom-iThumbHalf,
            rc.top+iThumbHalf, iLogPos);

   GetFaderThumbRect(hWnd, (LPRECT)&rc, (LPRECT)&thumb_rect, iLogPos);

   /* restore original brush */
   DeleteObject( SelectObject(hDC,hOldBrush) );

   /* now go to 3D button face brush */
   hOldBrush = SelectObject( hDC,
      CreateSolidBrush(GetSysColor(COLOR_BTNFACE)) );

   Rectangle(hDC, thumb_rect.left, thumb_rect.top,
            thumb_rect.right, thumb_rect.bottom);

   DeleteObject( SelectObject(hDC,hOldBrush) );
   if (iThumbHalf > 2)  /* Big enough for 3D paint */
      {
      DeleteObject( SelectObject(hDC,hOldPen) );
      hOldPen = SelectObject( hDC,
         CreatePen(PS_SOLID,1,GetSysColor(COLOR_WINDOW)) );
      MoveTo(hDC, thumb_rect.left+1, thumb_rect.bottom-1);
      LineTo(hDC, thumb_rect.left+1, thumb_rect.top+1);
      LineTo(hDC, thumb_rect.right-1, thumb_rect.top+1);
      DeleteObject( SelectObject(hDC,hOldPen) );

      hOldPen = SelectObject( hDC,
         CreatePen(PS_SOLID,1,GetSysColor(COLOR_BTNSHADOW)) );
      MoveTo(hDC, thumb_rect.right-1, thumb_rect.top+1);
      LineTo(hDC, thumb_rect.right-1, thumb_rect.bottom-1);
      LineTo(hDC, thumb_rect.left+1, thumb_rect.bottom-1);
      }
   /* restore original pen */
   DeleteObject( SelectObject(hDC,hOldPen) );
   DeleteObject( SelectObject(hDC,hOldBrush) );


   EndPaint(hWnd, &ps);
}



/* Thumb height is magically 1/8th of window height */

int GetThumbHeight(int iTotalHeight)
{
   return max(iTotalHeight/8, 2);
}


/* The fader button is always 1/8th the heighth of the window
   and 1/2 the width of the window */

void GetFaderThumbRect(HWND hWnd, LPRECT pRc,
     LPRECT pThumbRect, int iLogPos)
{
   int x_center;
   int iPhyPos;
   int iThumbHalf;
   LONG lLogRange;

   lLogRange = GetWindowLong(hWnd, FADER_RANGE);
   iThumbHalf = GetThumbHeight(pRc->bottom) / 2;
   iPhyPos = XlatPosLogicalToPhysical(lLogRange,
      pRc->bottom - iThumbHalf, pRc->top + iThumbHalf,
      iLogPos);
   x_center = pRc->right / 2;
   pThumbRect->left   = x_center / 2;
   pThumbRect->top    = iPhyPos - iThumbHalf;
   pThumbRect->right  = (3 * x_center) / 2;
   pThumbRect->bottom = iPhyPos + iThumbHalf;
}


/* This "helper" function maps from pixel coordinates to
   logical positions */

int XlatPosPhysicalToLogical(LONG lLogRange, int iPhysMax,
            int iPhysMin, int iPhyPos)
{
   int iLogMin;
   int iLogMax;
   int iResult;
   double dScale;

   iLogMax = HIWORD(lLogRange);
   iLogMin = LOWORD(lLogRange);
   dScale = (double)(iPhysMax - iPhyPos) /
            (double)(iPhysMax - iPhysMin);
   iResult = (int) ((double)iLogMin + (iLogMax-iLogMin)*(1-dScale));
   return iResult;
}


/* This "helper" function maps from logical positions to
   physical pixel coordinates */

int XlatPosLogicalToPhysical(LONG lLogRange, int iPhysMax,
            int iPhysMin, int iLogPos)
{
   int iLogMin;
   int iLogMax;
   int iResult;
   double dScale;

   iLogMax = HIWORD(lLogRange);
   iLogMin = LOWORD(lLogRange);

   dScale = (double)(iLogMax - iLogPos) /
            (double)(iLogMax - iLogMin);
   iResult = (int) ((double)iPhysMin + (iPhysMax-iPhysMin)*(1-dScale));
   return iResult;
}



/* The "caret" is a way of highlighting the controls which are
   active and inactive.  Note that a ones-complement is used so we
   don't need to know the previous state */

void DrawCaret(HDC hDC, LPRECT lprc)
{
   HBRUSH      hOldBrush;
   int iWidth;
   int iHeight;

   if (lprc->bottom - lprc->top < 4)
      return;

   /* initialize display context */
   hOldBrush = (HBRUSH)SelectObject( hDC,
      GetStockObject(GRAY_BRUSH) );

   /* draw caret */
   iWidth = lprc->right - lprc->left;
   iHeight = lprc->bottom - lprc->top;

   PatBlt( hDC, lprc->left+1, lprc->top+1, iWidth-2, 3, PATINVERT );
   PatBlt( hDC, lprc->right-4, lprc->top+1, 3, iHeight-2, PATINVERT );
   PatBlt( hDC, lprc->left+1, lprc->bottom-4, iWidth-2, 3, PATINVERT );
   PatBlt( hDC, lprc->left+1, lprc->top+1, 3, iHeight-2, PATINVERT );

   /* restore display context */
   SelectObject( hDC, hOldBrush );
}
