
/*
  glui.c
  Nate Robins, 1997.

  OpenGL based user interface.

 */

#include <stdio.h>
#include <stdlib.h>
#include <GL/glut.h>

#define GLUI_BORDER  3
#define GLUI_KNOB   40

#define GLUI_RAISED  1
#define GLUI_SUNKEN  0

#define GLUI_HORIZONTAL 0
#define GLUI_VERTICAL   1

#define GLUI_LESS -1
#define GLUI_HIT   0
#define GLUI_MORE  1

#define GLUI_HILITE 0.15

typedef struct _GLUIslider {
    int type;				/* vertical/horizontal */
    int parent;				/* parent of this slider */
    int window;				/* this sliders window */
    int win_x;				/* slider window x (parent relative) */
    int win_y;				/* slider window y (parent relative) */
    int win_w;				/* slider window width */
    int win_h;				/* slider window height */
    int length;				/* length of the slider in pixels */
    int knob;				/* position of the knob in pixels */
    int lit;				/* currently lit? */
    void (*update)(float);		/* callback for updating (returns %) */
    struct _GLUIslider* next;
} GLUIslider;

static GLUIslider* _gluiSliders = NULL;
static GLUIslider* _gluiHit = NULL;

static GLUIslider*
_gluiCurrentSlider(void)
{
    GLUIslider* slider = _gluiSliders;
    int window = glutGetWindow();

    while(slider) {
	if (slider->window == window)
	    break;
	slider = slider->next;
    }

    if (!slider)
	printf("glui: _gluiCurrentSlider() failed!\n");

    return slider;
}

static void
_gluiEmboss(int raised, int lit, int x, int y, int width, int height)
{
    int i;
    float c;

    for (i = 0; i < GLUI_BORDER; i++) {
	c = (float)i / (GLUI_BORDER * 5) + (lit ? GLUI_HILITE : 0.0);
	if (raised)
	    glColor3f(0.275+c, 0.2+c, 0.2+c);
	else
	    glColor3f(0.875-c, 0.8-c, 0.8-c);
	glBegin(GL_LINE_STRIP);
	glVertex2f(x+i+1, y+i);
	glVertex2f(x+width-i-1, y+i);
	glVertex2f(x+width-i-1, y+height-i);
	glEnd();
    }

    for (i = 0; i < GLUI_BORDER; i++) {
	c = (float)i / (GLUI_BORDER * 5);
	if (raised)
	    glColor3f(0.875-c, 0.8-c, 0.8-c);
	else
	    glColor3f(0.275+c, 0.2+c, 0.2+c);
	glBegin(GL_LINE_STRIP);
	glVertex2f(x+i, y+i);
	glVertex2f(x+i, y+height-i-1);
	glVertex2f(x+width-i-1, y+height-i-1);
	glEnd();
    }

    c = lit ? GLUI_HILITE : 0.0;

    if (raised)
	glColor3f(0.575+c, 0.5+c, 0.5+c);
    else
	glColor3f(0.475+c, 0.3+c, 0.3+c);
    glRectf(x+i, y+i, x+width-i, y+height-i);
}

static void
_gluiDisplay(void)
{
    int lit;
    GLUIslider* slider = _gluiCurrentSlider();

    lit = slider->lit ? GLUI_HILITE : 0.0;

    glClear(GL_COLOR_BUFFER_BIT);
    if (slider->type == GLUI_HORIZONTAL) {
	_gluiEmboss(GLUI_SUNKEN, 0, /* never lit */
		    0, 0, slider->length, slider->win_h);
	_gluiEmboss(GLUI_RAISED, slider->lit, 
		    slider->knob - GLUI_KNOB/2, GLUI_BORDER,
		    GLUI_KNOB + GLUI_BORDER, slider->win_h - GLUI_BORDER*2);
	/* XXX why is it GLUI_KNOB+GLUI_BORDER ? */
	glColor3f(0.975+lit, 0.9+lit, 0.9+lit);
	glBegin(GL_LINES);
	glVertex2f(slider->knob-2, GLUI_BORDER*2-1);
	glVertex2f(slider->knob-2, slider->win_h-GLUI_BORDER*2+1);
	glVertex2f(slider->knob+1, GLUI_BORDER*2-1);
	glVertex2f(slider->knob+1, slider->win_h-GLUI_BORDER*2+1);
	glVertex2f(slider->knob+4, GLUI_BORDER*2-1);
	glVertex2f(slider->knob+4, slider->win_h-GLUI_BORDER*2+1);
	glEnd();
	glColor3f(0.175+lit, 0.1+lit, 0.1+lit);
	glBegin(GL_LINES);
	glVertex2f(slider->knob-3, GLUI_BORDER*2-1);
	glVertex2f(slider->knob-3, slider->win_h-GLUI_BORDER*2+1);
	glVertex2f(slider->knob+0, GLUI_BORDER*2-1);
	glVertex2f(slider->knob+0, slider->win_h-GLUI_BORDER*2+1);
	glVertex2f(slider->knob+3, GLUI_BORDER*2-1);
	glVertex2f(slider->knob+3, slider->win_h-GLUI_BORDER*2+1);
	glEnd();
    } else {
	_gluiEmboss(GLUI_SUNKEN, 0, /* never lit */
		    0, 0, slider->win_w, slider->length);
	_gluiEmboss(GLUI_RAISED, slider->lit, 
		    GLUI_BORDER, slider->knob - GLUI_KNOB/2,
		    slider->win_w - GLUI_BORDER*2, GLUI_KNOB + GLUI_BORDER);
	/* XXX why is it GLUI_KNOB+GLUI_BORDER ? */
	glColor3f(0.175+lit, 0.1+lit, 0.1+lit);
	glBegin(GL_LINES);
	glVertex2f(GLUI_BORDER*2-1, slider->knob-2);
	glVertex2f(slider->win_w-GLUI_BORDER*2+1, slider->knob-2);
	glVertex2f(GLUI_BORDER*2-1, slider->knob+1);
	glVertex2f(slider->win_w-GLUI_BORDER*2+1, slider->knob+1);
	glVertex2f(GLUI_BORDER*2-1, slider->knob+4);
	glVertex2f(slider->win_w-GLUI_BORDER*2+1, slider->knob+4);
	glEnd();
	glColor3f(0.975+lit, 0.9+lit, 0.9+lit);
	glBegin(GL_LINES);
	glVertex2f(GLUI_BORDER*2-1, slider->knob-3);
	glVertex2f(slider->win_w-GLUI_BORDER*2+1, slider->knob-3);
	glVertex2f(GLUI_BORDER*2-1, slider->knob+0);
	glVertex2f(slider->win_w-GLUI_BORDER*2+1, slider->knob+0);
	glVertex2f(GLUI_BORDER*2-1, slider->knob+3);
	glVertex2f(slider->win_w-GLUI_BORDER*2+1, slider->knob+3);
	glEnd();
    }

    glutSwapBuffers();
}

static int
_gluiHitKnob(GLUIslider* slider, int x, int y)
{
    if (slider->type == GLUI_HORIZONTAL) {
	/* we know that we don't have to test the y coordinate because
	   the mouse came down in the window (this means that they can
	   hit the borders and still move the knob, but that's okay).
	 */
	if (x > slider->knob - GLUI_KNOB/2 && x < slider->knob + GLUI_KNOB/2)
	    return GLUI_HIT;
	else if (x < slider->knob)
	    return GLUI_LESS;
	else
	    return GLUI_MORE;
    } else {
	/* we know that we don't have to test the x coordinate because
	   the mouse came down in the window (this means that they can
	   hit the borders and still move the knob, but that's okay).
	 */
	if (y > slider->knob - GLUI_KNOB/2 && y < slider->knob + GLUI_KNOB/2)
	    return GLUI_HIT;
	else if (y < slider->knob)
	    return GLUI_LESS;
	else
	    return GLUI_MORE;
    }
}

static void
_gluiConstrainKnob(GLUIslider* slider)
{
    if (slider->knob > slider->length - GLUI_BORDER*2 - GLUI_KNOB/2)
	slider->knob = slider->length - GLUI_BORDER*2 - GLUI_KNOB/2;
    else if (slider->knob < GLUI_BORDER + GLUI_KNOB/2)
	slider->knob = GLUI_BORDER + GLUI_KNOB/2;
}


static float
_gluiKnobPercent(GLUIslider* slider)
{
    return (float)(slider->knob - GLUI_KNOB/2 - GLUI_BORDER) /
	(slider->length - GLUI_BORDER*3 - GLUI_KNOB);
}

static int
_gluiKnobPosition(GLUIslider* slider, float percent)
{
    return GLUI_BORDER + GLUI_KNOB/2 + percent * 
	(slider->length - GLUI_BORDER*3 - GLUI_KNOB);
}

static int _gluiX;
static int _gluiY;
static int _gluiMouseDown;

static void
_gluiTimer(int value)
{
    GLUIslider* slider = (GLUIslider*)value;
    float percent;

    percent = _gluiKnobPercent(slider); 

    if (_gluiMouseDown != 0 && percent > 0.0 && percent < 1.0) {
	if (_gluiMouseDown == GLUI_LESS) {
	    slider->knob -= slider->length / 25.0;
	    _gluiConstrainKnob(slider);
	} else {
	    slider->knob += slider->length / 25.0;
	    _gluiConstrainKnob(slider);
	}
	glutSetWindow(slider->window);
 	glutPostRedisplay();
	slider->update(_gluiKnobPercent(slider));
	glutTimerFunc(20, _gluiTimer, (int)slider);
    }
}

static void
_gluiConvertY(GLUIslider* slider, int* y)
{
    if (slider->win_h < 0) {
	glutSetWindow(slider->parent);
	*y = glutGet(GLUT_WINDOW_HEIGHT) + slider->win_h - slider->win_y - *y;
	glutSetWindow(slider->window);
    } else {
	*y = slider->win_h - *y;
    }
}

/* ARGSUSED */
static void
_gluiMouse(int button, int state, int x, int y)
{
    GLUIslider* slider = _gluiCurrentSlider();
    int side;

    _gluiConvertY(slider, &y);

    _gluiX = x;
    _gluiY = y;
    _gluiHit = NULL;
    _gluiMouseDown = GL_FALSE;

    if (state == GLUT_DOWN) {
	side = _gluiHitKnob(slider, x, y);
	if (side == GLUI_HIT) {
	    _gluiHit = slider;
	} else if (side == GLUI_LESS) {
	    slider->knob -= slider->length / 25.0;
	    _gluiConstrainKnob(slider);
	} else {
	    slider->knob += slider->length / 25.0;
	    _gluiConstrainKnob(slider);
	}
	glutPostRedisplay();
	slider->update(_gluiKnobPercent(slider));
	_gluiMouseDown = side;
	if (side != 0) {
	    glutTimerFunc(500, _gluiTimer, (int)slider);
	}
    } else {
	slider->lit = GL_FALSE;
    }
}

static void
_gluiMotion(int x, int y)
{
    GLUIslider* slider = _gluiHit;

    if (slider) {
	_gluiConvertY(slider, &y);

	if (slider->type == GLUI_HORIZONTAL) {
	    /* clamp the incoming old position, or else the knob will
               possibly "jump" due to the false delta. */
	    if (_gluiX < GLUI_BORDER+1)
		_gluiX = GLUI_BORDER+1;
	    if (_gluiX > slider->length - GLUI_BORDER*2)
		_gluiX = slider->length - GLUI_BORDER*2;
	    /* we don't want to take any action if the mouse pointer
               has moved passed the extents of the slider. */
	    if (x > GLUI_BORDER && x < slider->length - GLUI_BORDER*2) {
		slider->knob -= _gluiX - x;
		_gluiX = x;
	    }
	} else {
	    /* clamp the incoming old position, or else the knob will
               possibly "jump" due to the false delta. */
	    if (_gluiY < GLUI_BORDER+1)
		_gluiY = GLUI_BORDER+1;
	    if (_gluiY > slider->length - GLUI_BORDER*2)
		_gluiY = slider->length - GLUI_BORDER*2;
	    /* we don't want to take any action if the mouse pointer
               has moved passed the extents of the slider. */
	    if (y > GLUI_BORDER && y < slider->length - GLUI_BORDER*2) {
		slider->knob -= _gluiY - y;
		_gluiY = y;
	    }
	}
	_gluiConstrainKnob(slider);

	/* post a display _before_ updating the user, so that the knob
           won't lag behind. */
	glutPostRedisplay();

	/* make sure to set the parent window current, otherwise if
           there is OpenGL state being changed in the update callback,
           it will be done to the sliders context! */
	glutSetWindow(slider->parent);
	slider->update(_gluiKnobPercent(slider));
    }
}

static void
_gluiPassive(int x, int y)
{
    GLUIslider* slider = _gluiCurrentSlider();

    _gluiConvertY(slider, &y);

    if (_gluiHitKnob(slider, x, y) == 0)
	slider->lit = GL_TRUE;
    else
	slider->lit = GL_FALSE;

    glutPostRedisplay();
}

/* ARGSUSED */
static void
_gluiEntry(int state)
{
    GLUIslider* slider = _gluiCurrentSlider();

    /* set the lit flag to false whether we are coming or going
       because if we are doing either, we can't be on top of the knob!  */
    slider->lit = GL_FALSE;
    glutPostRedisplay();
}

void
gluiReshape(int width, int height)
{
    float percent;
    int x, y, w, h;
    GLUIslider* slider = _gluiSliders;

    while (slider) {
	/* we need to get the width and height of the parent, so set
           it current. */
	glutSetWindow(slider->parent);

	/* all this mumbo jumbo takes care of the negative arguments
           to attach the slider to different sides of the window. */
	x = slider->win_x;
	if (x < 0)
	    x = width - slider->win_w + x + 1;
	y = slider->win_y;
	if (y < 0)
	    y = height - slider->win_h + y + 1;
	w = slider->win_w;
	if (w < 0)
	    w = glutGet(GLUT_WINDOW_WIDTH) + slider->win_w - slider->win_x;
	h = slider->win_h;
	if (h < 0)
	    h = glutGet(GLUT_WINDOW_HEIGHT) + slider->win_h - slider->win_y;

	glutSetWindow(slider->window);
	glutPositionWindow(x, y);
	glutReshapeWindow(w, h);

 	percent = _gluiKnobPercent(slider);

	if (slider->type == GLUI_HORIZONTAL)
	    slider->length = w;
	else
	    slider->length = h;

	slider->knob = _gluiKnobPosition(slider, percent);

	_gluiConstrainKnob(slider);

	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluOrtho2D(0, w, 0, h);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	
	slider = slider->next;
    }
}



int
gluiVerticalSlider(int parent, int x, int y, int width, int height,
		   float percent, void (*update)(float))
{
    GLUIslider* slider = (GLUIslider*)malloc(sizeof(GLUIslider));
    slider->next = _gluiSliders;
    _gluiSliders = slider;

    slider->type = GLUI_VERTICAL;
    slider->parent = parent;
    slider->window = glutCreateSubWindow(parent, x, y, width, height);
    slider->win_x = x;
    slider->win_y = y;
    slider->win_w = width;
    slider->win_h = height;
    slider->update = update;
    slider->lit = GL_FALSE;

/*     glutSetCursor(GLUT_CURSOR_LEFT_RIGHT); */
    glutDisplayFunc(_gluiDisplay);
    glutEntryFunc(_gluiEntry);
    glutMouseFunc(_gluiMouse);
    glutMotionFunc(_gluiMotion);
    glutPassiveMotionFunc(_gluiPassive);

    glDisable(GL_LIGHTING);
    glDisable(GL_DEPTH_TEST);

    slider->length = height;
    if (height < 0) {
	glutSetWindow(parent);
	slider->length = glutGet(GLUT_WINDOW_HEIGHT) + height - slider->win_y;
    }

    slider->knob = _gluiKnobPosition(slider, percent);
    _gluiConstrainKnob(slider);

    return slider->window;
}


/* On a horizontal slider, the height must be non-negative. */

int
gluiHorizontalSlider(int parent, int x, int y, int width, int height,
		     float percent, void (*update)(float))
{
    GLUIslider* slider = (GLUIslider*)malloc(sizeof(GLUIslider));
    slider->next = _gluiSliders;
    _gluiSliders = slider;

    slider->type = GLUI_HORIZONTAL;
    slider->parent = parent;
    slider->window = glutCreateSubWindow(parent, x, y, width, height);
    slider->win_x = x;
    slider->win_y = y;
    slider->win_w = width;
    slider->win_h = height;
    slider->update = update;
    slider->lit = GL_FALSE;

/*     glutSetCursor(GLUT_CURSOR_LEFT_RIGHT); */
    glutDisplayFunc(_gluiDisplay);
    glutEntryFunc(_gluiEntry);
    glutMouseFunc(_gluiMouse);
    glutMotionFunc(_gluiMotion);
    glutPassiveMotionFunc(_gluiPassive);

    glDisable(GL_LIGHTING);
    glDisable(GL_DEPTH_TEST);

    slider->length = width;
    if (width < 0) {
	glutSetWindow(parent);
	slider->length = glutGet(GLUT_WINDOW_WIDTH) + width - slider->win_x;
    }

    slider->knob = _gluiKnobPosition(slider, percent);
    _gluiConstrainKnob(slider);

    return slider->window;
}
