;; -*- Mode:Lisp; Package:CLIO-OPEN; Base:10; Lowercase:T; Fonts:(CPTFONT); Syntax:Common-Lisp; -*- ;;;----------------------------------------------------------------------------------+ ;;; | ;;; TEXAS INSTRUMENTS INCORPORATED | ;;; P.O. BOX 149149 | ;;; AUSTIN, TEXAS 78714 | ;;; | ;;; Copyright (C) 1989, 1990 Texas Instruments Incorporated. | ;;; | ;;; Permission is granted to any individual or institution to use, copy, modify, and | ;;; distribute this software, provided that this complete copyright and permission | ;;; notice is maintained, intact, in all copies and supporting documentation. | ;;; | ;;; Texas Instruments Incorporated provides this software "as is" without express or | ;;; implied warranty. | ;;; | ;;;----------------------------------------------------------------------------------+ (in-package "CLIO-OPEN") (export '( make-slider slider ) 'clio-open) (defmacro translate-x-to-y (x x-width slider) "Translate x coord for horizontal slider into y of a vertical slider. X-WIDTH is the width in x-direction that must be changed into a y-offset." `(with-slots (height) ,slider (- height 1 ,x (max 0 (1- ,x-width))))) (defmacro confine-to (value minimum maximum) `(max ,minimum (min ,value ,maximum))) (defmacro align (value increment) ;; Since we are talking SCALE VALUE here ;; we pixel-round since it may be a REAL number. `(if (= 1 ,increment) ,value (* (pixel-round ,value ,increment) ,increment))) (defmacro value-length (value minimum) `(- ,value ,minimum)) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Slider | ;;; | ;;;----------------------------------------------------------------------------+ ;; Implementation Strategy: ;; ;; Since CLIO should implement a "look and ;; feel" independent implemementation of open-look, then only those ;; parts of the slider that are going to exist in most implemementations ;; will be supported. Since make-slider accepts : ;; (increment indicator-size maximum minimum orientation update-delay value compress-exposures) ;; then all "features" must be derived from these inputs. ;; ;; To provide numeric visual feedback of the current value is desirable, ;; but providing this as a typein field or read-only field really requires a label ;; or else the displayed result is somewhat confusing. ;; The current value will be implemented as AUTOMATIC tick-marks and tick-mark ;; labels based on the min-max values and the space available to print them. The ;; actual current value will not be printed but will be discernable by "reading the scale". ;; ;; Thus the slider parts implemented are : ;; (bar, drag-box, (automatic) tick-marks, (automatic) tick-text) ;; and the following will NOT be provided : ;; (End boxes, labels, typein fields, non-numeric text of any kind) ;; This means that the read-only min-max current-value fields will be provided only by way ;; of the min-/max tick-mark tick-text labels. ;; ;; When horizontal sliders require max (or min) values of more than 2 digits ;; then the tick-mark & tick-mark-number-labels are difficult to display. In this ;; case a :vertical orientation is more appropriate. If more than 2-digits are used ;; for a :horizontal slider then the tick-mark granularity will be reduced in order ;; to accommodate the width of the digits. ;; (defcontact slider (core contact) ((increment :type number :reader scale-increment ;; SETF method defined below :initarg :increment :initform 1) (indicator-size ;; The size of the distance between tick-marks in value units. ;; :off means "no tickmarks or tick labels", 1 will cause tick-mark ;; overlap if there is not enough space to display. [2..N] will ;; cause a tick-spacing of [1..(1- N)]. ;; :type (or number (member :off)) ;; 0 means "automatic" tick mark spacing. :reader scale-indicator-size ;; SETF method defined below :initarg :indicator-size :initform 0) (maximum :type number :reader scale-maximum ;; SETF method defined below :initarg :maximum :initform 1) (minimum :type number :reader scale-minimum ;; SETF method defined below :initarg :minimum :initform 0) (orientation :type (member :horizontal :vertical) :reader scale-orientation ;; SETF method defined below :initarg :orientation :initform :horizontal) (update-delay :type (or number (member :until-done)) :reader scale-update-delay ;; SETF method defined below :initarg :update-delay :initform 0) (value :type number :reader scale-value ;; SETF method defined below :initarg :value :initform 0) (compress-exposures :initform :on :type (member :off :on) :reader contact-compress-exposures :allocation :class) ;; Internal storage slots (font :type fontable);; font for current scale (min-text-width :type number) ;; pixel width of minimum value print string (max-text-width :type number) ;; pixel width of maximum value print string (dimensions :type list) ;; (getf *slider-dimensions* scale) (middle-length :type number) ;; pixel length between first & last tick marks ) (:resources increment indicator-size maximum minimum orientation update-delay value (border-width :initform 0) (event-mask :initform #.(make-event-mask :exposure :pointer-motion-hint)))) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Setf Accessors | ;;; | ;;;----------------------------------------------------------------------------+ (defmethod (setf scale-orientation) (new-orientation (slider slider)) (with-slots (orientation width height) slider (unless (eq orientation new-orientation) (check-type new-orientation (member :horizontal :vertical)) (setf orientation new-orientation) (multiple-value-bind (new-width new-height) (preferred-size slider :width height :height width) (change-geometry slider :width new-width :height new-height :accept-p t)))) new-orientation) (defmethod (setf scale-update-delay) (new-update-delay (slider slider)) (with-slots (update-delay) slider (assert (or (eq new-update-delay :until-done) (and (numberp new-update-delay) (not (minusp new-update-delay)))) (new-update-delay) "~a is neither :UNTIL-DONE or a non-negative number." new-update-delay) (setf update-delay new-update-delay))) (defmethod (setf scale-value) (new-value (slider slider)) (scale-update slider :value new-value) new-value) (defmethod (setf scale-minimum) (new-minimum (slider slider)) (scale-update slider :minimum new-minimum) new-minimum) (defmethod (setf scale-maximum) (new-maximum (slider slider)) (scale-update slider :maximum new-maximum) new-maximum) (defmethod (setf scale-increment) (new-increment (slider slider)) (scale-update slider :increment new-increment) new-increment) (defmethod (setf scale-indicator-size) (new-indicator-size (slider slider)) (scale-update slider :indicator-size new-indicator-size) new-indicator-size) ;;;------------------------------------------------------------------------------------+ ;;; | ;;; Helper Functions | ;;; | ;;;------------------------------------------------------------------------------------+ (defun slider-tick-mark-thickness (slider) (if (eq :extra-large (contact-scale slider)) 3 2)) (defun slider-bar-tick-gap (slider) ;; Distance top of tick-mark and nearest point on bar (1+ (case (contact-scale slider) (:small 1) (:medium 2) (:large 3) (:extra-large 4)))) (defun slider-margin (slider margin) "Returns the MARGIN of SLIDER, one of :min :top :text :max" ;; This is initially *slider-default-margin* until ;; after the PREFERRED-SIZE method is called. Then margins include ;; any additional increase due to a width or height larger than the ;; preferred size. :LEFT means the left margin for this particular orientation. (assert (member margin '(:min :top :text :max)) (margin) "~a is an illegal margin" margin) (let ((margins (getf (getf (window-plist slider) :slider-info) :margins))) (or (getf margins margin) ;; Calling before margins are setup is never ;; done but code is here for completeness *slider-default-margin*))) (defun first-tick-offset (slider) ;; Offset, not including (slider-margin slider :min), from ;; min edge of contact to CENTERLINE of first tick-mark. (with-slots (min-text-width orientation font dimensions indicator-size) slider (let ((tick-mark-offset (slidebar-tick-mark-offset dimensions)) (gap (slidebar-gap dimensions))) (+ *slider-default-margin* ;; Add GAP below since drag-box clear-gap-around extends past bar edge (if (eq :off indicator-size) (+ gap tick-mark-offset) (if (eq orientation :horizontal) (max (floor min-text-width 2) (+ gap tick-mark-offset)) (+ gap tick-mark-offset) ;; the text baseline is ALWAYS above end of bar MIN )))))) (defun last-tick-offset (slider) ;; Offset, not including (slider-margin slider :max), from ;; max edge of contact to CENTERLINE of last tick-mark. (with-slots (max-text-width orientation font dimensions indicator-size) slider (let ((tick-mark-offset (slidebar-tick-mark-offset dimensions)) (gap (slidebar-gap dimensions))) (+ *slider-default-margin* ;; Add GAP below since drag-box clear-gap-around extends past bar edge (if (eq :off indicator-size) (+ gap tick-mark-offset) (if (eq orientation :horizontal) (max (ceiling max-text-width 2) (+ gap tick-mark-offset)) (+ tick-mark-offset (max gap ;; font-ascent may go beyond end of bar MAX if font is bigger than "point" requested (abs (- (cadr (getf (slidebar-bar-text-offset dimensions) orientation)) (max-char-ascent font) )))))))))) ;; ;; ;; Pixels : Scale Units : ;; ;; middle-length (- maximum minimum) ;; ;; [MAX] ;; | ;; | ;; | proportional [MAX] ;; | - | ;; | | ;; | Pixel-delta | - ;; | | Scale-delta ;; | | ;; [MIN] - [MIN] - ;; ;; 0 minimum ;; ;; Since : ;; ;; pixel-delta / middle-length = scale-delta / (- maximum minimum) ;; ;; Then : ;; ;; Pixel-delta = (* scale-delta middle-length) / (- maximum minimum) ;; ;; And : ;; ;; Scale-delta = (* pixel-delta (- maximum minimum)) / middle-length ;; (defun units-to-pixels (slider scale-delta) ;; Convert a scale delta to a pixel delta (with-slots (minimum maximum middle-length) slider (pixel-round (/ (* scale-delta middle-length) (- maximum minimum))))) (defun pixels-to-units (slider pixel-delta) (with-slots (orientation minimum maximum increment middle-length) slider ;; Convert a pixel delta to a scale units delta ;; *DON'T* round this - units may be fractional ! (/ (* pixel-delta (- maximum minimum)) middle-length))) ;; NOTE: The functions named ????-x and below return values strictly for a :horizontal ;; slider and the return values must be translated for a :vertical slider. (defun first-tick-x (slider) (+ (slider-margin slider :min) (first-tick-offset slider))) (defun drag-box-center-x (slider &optional (scale-value (scale-value slider))) (with-slots (minimum) slider ;; Returns dead center of drag-box (+ (first-tick-x slider) ;; Must subtract minimum since minimum can be negative and is NOT always zero! (units-to-pixels slider (value-length scale-value minimum))))) (defun drag-box-min-x (slider &optional (scale-value (scale-value slider))) (with-slots (dimensions minimum maximum) slider (let* ((drag-box-width (slidebar-drag-box-width dimensions)) (gap (slidebar-gap dimensions))) (- (drag-box-center-x slider scale-value) (floor drag-box-width 2) gap ;; subtract whitespace gap around drag-box )))) (defun drag-box-position (slider &optional (scale-value (scale-value slider))) (declare (values x y width height)) (with-slots (orientation ) slider ;; Return values describing area of drag-box for SCALE-VALUE (let* ((drag-min-edge (drag-box-min-x slider scale-value)) (drag-image (getf (getf *slider-drag-box-images* orientation) (contact-scale slider)))) (if (eq orientation :horizontal) (values drag-min-edge (+ (slider-margin slider :top) *slider-default-margin*) (image-width drag-image) (image-height drag-image)) (values (+ (slider-margin slider :top) *slider-default-margin*) (translate-x-to-y drag-min-edge (image-height drag-image) slider) (image-width drag-image) (image-height drag-image))) ))) (defmethod scale-update ((slider slider) &key value minimum maximum indicator-size increment) ;; Called by (method initialize-instance :after (slider)) to do error checking, and by ;; SETF methods for slots in arglist above, and by (setf scale-value) called to move slider. (with-slots ((current-val value) (current-min minimum) (current-max maximum) (current-ind indicator-size) (current-inc increment) orientation min-text-width max-text-width font) slider (let ((old-val (and value current-val)) ;; old-value & flag that value was passed in. (old-min (and minimum current-min)) (old-max (and maximum current-max)) (old-inc (and increment current-inc)) (old-ind (and indicator-size current-ind))) (setf minimum (or minimum current-min) maximum (or maximum current-max) value (or value (confine-to current-val minimum maximum)) indicator-size (or indicator-size current-ind) increment (or increment current-inc)) (assert (and (numberp minimum) (numberp maximum) (< minimum maximum)) (minimum maximum) "Minimum (~a) is not less than maximum (~a)." minimum maximum) (assert (and (numberp value) (<= minimum value maximum)) (value) "Value (~a) must be in the range [~a, ~a]." value minimum maximum) (assert (or (eq :off indicator-size) (and (numberp indicator-size) (not (minusp indicator-size)))) (indicator-size) "Indicator-size (~a) must be :OFF, 0, or a positive number." indicator-size) (assert (and (numberp increment) (< 0 increment (1+ (- maximum minimum))) ;; allow fractional increments, allow increment = maximum (zerop (mod (- maximum minimum) increment))) (increment) "Increment (~a) must be in the range [0 ~a] and a factor of ~:*~d." increment (- maximum minimum)) ;; Once VALUE & INCREMENT are valid we can align VALUE, if necessary, ;; to be a multiple of INCREMENT. (setq value (+ minimum (align (value-length value minimum) increment))) (setf current-min minimum current-max maximum current-val value current-ind indicator-size current-inc increment min-text-width (text-extents font (format nil "~a" minimum)) max-text-width (text-extents font (format nil "~a" maximum))) ;; Redisplay drag-box and any changes (when (realized-p slider) (cond ((or (and old-min (not (= old-min current-min))) (and old-max (not (= old-max current-max))) (and old-ind (not (eq old-ind current-ind))) (and old-inc (not (= old-inc current-inc)))) (clear-area slider :exposures-p t)) ((and old-val ;; when called with NEW increment value (not (= old-val current-val))) ;; when something has changed ;; Compute area of old drag-box ( if any ) (multiple-value-bind (old-x old-y old-width old-height) (drag-box-position slider old-val) ;; Compute area of new drag-box (multiple-value-bind (x y width height) (drag-box-position slider current-val) ;; Merge areas to redisplay : new drag-box, bar between old & new, ;; old drag-box ( if any ), & tick marks obscured by drag-box (when old-val (if (eq orientation :horizontal) (setf width (+ (abs (- x old-x)) (max old-width width)) x (min x old-x)) (setf height (+ (abs (- y old-y)) (max old-height height)) y (min y old-y)))) (clear-area slider :x x :y y :width width :height height) (display slider x y width height)))) (t))) ))) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Initialization | ;;; | ;;;----------------------------------------------------------------------------+ (defun make-slider (&rest initargs &key &allow-other-keys) (apply #'make-contact 'slider initargs)) (defun bar-bottom-offset (slider) ;; Offset from the top (left) of horizontal (vertical) slider ;; ... does NOT include (slider-margin slider :top) ... ;; and 1-pixel past the bottom (right) edge of the slidebar (with-slots (dimensions) slider (+ *slider-default-margin* (slidebar-gap dimensions) (slidebar-bar-drag-offset dimensions) (slidebar-bar-thickness dimensions)))) (defun fixed-thickness (slider &key include-text-p) ;; The minimum thickness of slider required for the ;; scale, orientation, and string characteristics of the minimum & maximum (with-slots (orientation min-text-width max-text-width indicator-size dimensions font) slider (let ((x (first (getf (slidebar-bar-text-offset dimensions) orientation))) (y (second (getf (slidebar-bar-text-offset dimensions) orientation))) (scale (contact-scale slider))) (if (eq :off indicator-size) (+ *slider-default-margin* (if (eq orientation :horizontal) (image-height (getf (getf *slider-drag-box-images* orientation) scale)) (image-width (getf (getf *slider-drag-box-images* orientation) scale))) *slider-default-margin*) ;; no space allocated for tick marks & text ;; else (if (eq orientation :horizontal) (+ (bar-bottom-offset slider) y (if include-text-p (+ (max-char-descent font) *slider-default-margin*) 0)) (+ (bar-bottom-offset slider) x (if include-text-p (+ (max min-text-width max-text-width) *slider-default-margin*) 0))))))) (defun slider-compute-margins (slider) ;; Now margins can be computed from the delta between the size needed and the ;; size we were given. The length of the slider basically stretches to fit but ;; any extra height results in the slider being centered in space provided. ;; PREFERRED-SIZE (via initialize-instance :after) MUST have been called to ;; set WIDTH & HEIGHT by this time. (with-slots (orientation width height middle-length) slider (let* ((total-min-thickness (fixed-thickness slider :include-text-p t)) (size (if (eq orientation :horizontal) height width)) (top-margin (floor (- size total-min-thickness) 2)) (bottom-margin (- size total-min-thickness top-margin))) (setf (getf (getf (window-plist slider) :slider-info) :margins) ;; left top bottom right (horizontal) (list :min 0 :top top-margin :text bottom-margin :max 0)) ;; With margins set we can now compute and save middle-length for efficiency (setf middle-length (- (if (eq :horizontal orientation) width height) (slider-margin slider :min) (first-tick-offset slider) (last-tick-offset slider) (slider-margin slider :max) ))))) (defmethod initialize-instance :after ((slider slider) &key &allow-other-keys) (with-slots (font width height minimum maximum dimensions min-text-width max-text-width) slider (setq font (find-font slider *default-display-text-font*) dimensions (getf *slider-dimensions* (contact-scale slider))) (scale-update slider) ;; do some error checking, set min-text-width, etc. ;; Initialize required geometry (multiple-value-setq (width height) (preferred-size slider)) ;; Compute margins now that WIDTH & HEIGHT are known (slider-compute-margins slider) )) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Geometry Management | ;;; | ;;;----------------------------------------------------------------------------+ (DEFMETHOD rescale :before ((slider slider)) (with-slots (font dimensions) slider (setf font (find-font slider *default-display-text-font*) dimensions (getf *slider-dimensions* (contact-scale slider))) (slider-compute-margins slider) )) (defmethod resize :after ((slider slider) new-width new-height new-border-width) ;; This method duplicates calculations started in (method initialize-instance :after (slider)) ;; but are done here since they also must be performed when change-geometry is invoked. ;; Called when window-manager or someone else calls change-geometry. (declare (ignore new-width new-height new-border-width)) (slider-compute-margins slider)) (defmethod preferred-size ((slider slider) &key width height border-width) (declare (ignore border-width)) ;; preferred-border-width is 0 (with-slots (orientation min-text-width max-text-width font dimensions indicator-size (current-height height) (current-width width)) slider (let* ((drag-box-width (slidebar-drag-box-width dimensions)) (tick-mark-offset (slidebar-tick-mark-offset dimensions)) ;; Min width of slider with 2 positions = double size of drag box (minimum-double-width (+ (- (first-tick-offset slider) tick-mark-offset) (max (* 2 drag-box-width) (if (eq :off indicator-size) 0 (if (eq orientation :horizontal) ;; room needed to display text between first/last-tick (+ (ceiling min-text-width 2) (max-char-descent font) (floor max-text-width 2)) ;; room needed to display 2 text lines (min & max) vertically, ;; plus a small gap between (+ (max-char-ascent font) (max-char-descent font) ;; gap between (max-char-ascent font))))) (- (last-tick-offset slider) tick-mark-offset))) ;; Calculate geometry assuming :horizontal orientation (preferred-height (max ;; Suggested or current height (if (eq orientation :horizontal) (or height current-height) (or width current-width)) ;; Total thickness of horizontal bar (fixed-thickness slider :include-text-p t))) (preferred-width (max ;; Suggested or current width (if (eq orientation :horizontal) (or width current-width) (or height current-height)) minimum-double-width)) ) ;; Return preferred geometry according to actual orientation (if (eq orientation :horizontal) ;; preferred-border-width is always 0 (values preferred-width preferred-height 0) (values preferred-height preferred-width 0)) ))) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Event Translations | ;;; | ;;;----------------------------------------------------------------------------+ (defevent slider (:button-press :button-1) slider-press) (defevent slider (:button-release :button-1) slider-release) (defevent slider (:motion-notify :button-1) slider-handle-motion) (defun slider-release (slider) (declare (special *slider-pressed-p*)) (when (boundp '*slider-pressed-p*) (throw-action slider :release t))) (defun highlite-drag-box (slider gc x y width height gap) (draw-rectangle slider gc (+ gap 1 gap x) (+ gap 1 gap y) (- width (* 4 gap) 3) (- height (* 4 gap) 3) :fill-p)) (defun slider-press (slider) (with-event (x y) (with-slots (foreground orientation update-delay minimum maximum increment width height display value dimensions) slider (let (*slider-pressed-p* ) (declare (special *slider-pressed-p*)) (multiple-value-bind (drag-x drag-y drag-width drag-height) (drag-box-position slider) (when (cond ((and (>= x drag-x) (< x (+ drag-x drag-width)) (>= y drag-y) (< y (+ drag-y drag-height))) ;; SELECT on drag box (let ((*highlight-pixel* (logxor foreground (contact-current-background-pixel slider))) (gap (slidebar-gap dimensions))) (declare (special *highlight-pixel*)) ;; use this in display method while moving .. (using-gcontext (gc :drawable slider :function boole-xor :foreground *highlight-pixel*) ;; Highlight drag area (highlite-drag-box slider gc drag-x drag-y drag-width drag-height gap) ;; Set timer for update (when (and (numberp update-delay) (plusp update-delay)) (add-timer slider :update-delay update-delay)) (apply-callback slider :begin-continuous) (catch :release (let ((*previous-position* (if (eq :vertical orientation) y x))) (declare (special *previous-position*)) (loop (process-next-event display)))) (apply-callback slider :end-continuous) ;; Unhighlight drag area. (multiple-value-bind (new-drag-x new-drag-y) (drag-box-position slider) (highlite-drag-box slider gc new-drag-x new-drag-y drag-width drag-height gap)))) t) ;; SELECT on bar ;; Since it is NOT in drag-box, just check if it is in bar ;; or the area of the bar if it had the thickness of the drag-box. ;; This makes clicking somewhat easier. ((multiple-value-bind (bar-x bar-y bar-width bar-height) (if (eq orientation :horizontal) (values (slider-margin slider :min) (slider-margin slider :top) (- width (slider-margin slider :min) (slider-margin slider :max)) drag-height) (values (slider-margin slider :top) (slider-margin slider :min) drag-width (- height (slider-margin slider :min) (slider-margin slider :max)))) (and (>= x bar-x) (>= y bar-y) (< x (+ bar-x bar-width)) (< y (+ bar-y bar-height)))) ;; Advance drag-box one increment in direction indicated. ;; User may click so fast that the drag box passes the click ;; position, thus inadvertently reversing the increment direction. ;; Synchronize by using current pointer position, not click position. (multiple-value-bind (ptr-x ptr-y) (pointer-position slider) (let ((delta (if (if (eq orientation :horizontal) (< ptr-x drag-x) (>= ptr-y (+ drag-y drag-height))) (- increment) increment)) (gap (slidebar-gap dimensions))) (slider-increment-value slider delta) ;; Must warp pointer to stay in MIN (or MAX) bar, if necessary (multiple-value-bind (new-drag-x new-drag-y drag-width drag-height) (drag-box-position slider) (multiple-value-bind (warp-x warp-y) (if (eq orientation :horizontal) (if (plusp delta) (let ((min-x (min (1- width) (+ new-drag-x drag-width gap)))) (when (< ptr-x min-x) (values min-x ptr-y))) (let ((max-x (max 0 (- new-drag-x gap)))) (when (< max-x ptr-x) (values max-x ptr-y)))) (if (minusp delta) (let ((min-y (min (1- height) (+ new-drag-y drag-height gap)))) (when (< ptr-y min-y) (values ptr-x min-y))) (let ((max-y (max 0 (- new-drag-y gap)))) (when (< max-y ptr-y) (values ptr-x max-y))))) (when warp-x (warp-pointer slider warp-x warp-y)))))) t)) ;; Report final value, if necessary (unless (eql 0 update-delay) (delete-timer slider :update-delay) (apply-callback slider :new-value value)))))))) (defun slider-increment-value (slider scale-increment) "Convert the scale-increment to a (possibly) new scale position and (possibly) cause the slider to be updated." (with-slots (value orientation increment minimum maximum update-delay) slider ;; Must use truncate for negative scale-increment's - rounds to zero. (let* ((new-value (+ value scale-increment)) (adjusted (confine-to (or (apply-callback slider :adjust-value new-value) new-value) minimum maximum))) (unless (= value adjusted) ;; unless no change in slider scale occurs (setf (scale-value slider) adjusted) ;; <- this calls scale-update & redisplays slider (when (eql 0 update-delay) (apply-callback slider :new-value adjusted)))))) (defun slider-handle-motion (slider) (declare (special *previous-position*)) (when (boundp '*previous-position*) (with-slots (orientation increment) slider (with-event (state x y) (multiple-value-bind (ptr-x ptr-y) ;; Is :button-1 still down? (if (plusp (logand state #.(make-state-mask :button-1))) ;; Yes, query current pointer position (pointer-position slider) ;; No, use final x,y returned for button transition (values x y)) (let ((modulo-increment (* (truncate (pixels-to-units slider (if (eq :horizontal orientation) (- ptr-x *previous-position*) ;; Must swap order of subtraction since positive y direction ;; is negative scale direction for :vertical slider (- *previous-position* ptr-y))) increment) increment))) ;; Convert the pixel motion to a suitable slider scale motion (unless (zerop modulo-increment) (slider-increment-value slider modulo-increment) ;; Use drag-box position. Ptr position is only correct if the drag-box can ;; move to the ptr posiiton without bumping up against the min/max limits. (setf *previous-position* (if (eq orientation :horizontal) (drag-box-center-x slider) (translate-x-to-y (drag-box-center-x slider) 1 slider)) )))))))) (defun choose-indicator-size (slider) "Returns TICK-LIMIT = the number of ticks to draw." ;; Called when indicator-size eq :off to automatic tick-marks (declare (values tick-limit increments-in-tick)) (with-slots (maximum minimum increment) slider (let* ((tick-mark-thickness (slider-tick-mark-thickness slider)) (increments-in-tick 1) (min-visible-width (* 2 tick-mark-thickness)) (ticks (floor (- maximum minimum) increment)) ) ;; Return appropriate tick-limit (values (1+ ;; 1+ since we draw the first-tick plus any calculated ticks (do* ((ticks-visible nil)) ((cond ((<= ticks 1) (setq ticks-visible 1)) ;; reached minimum ticks, 1 at each end ((>= (units-to-pixels slider (* increments-in-tick increment)) min-visible-width) (setq ticks-visible ticks))) ;; Exit form (return ticks-visible)) (setq ticks (floor ticks 2) increments-in-tick (* 2 increments-in-tick)))) increments-in-tick ;; 2nd return value )))) ;;;----------------------------------------------------------------------------+ ;;; | ;;; Display | ;;; | ;;;----------------------------------------------------------------------------+ ;;; d1 = first-tick-x ;;; d2 = slidebar-tick-mark-offset ;;; ;;; min fill drag empty max ;;; +-----------------------------------+ - ;;; | | gap ;;; | +----+ | - ;;; | | || | drag-bar-offset ;;; | .-- ----- | || -------- --. | - ;;; | |** ***** | || | | bar-thickness ;;; | `-- ----- | || -------- --' | - ;;; | |---+| | ;;; |<-----> +----+ | ;;; | d1 || || || | bar-text-offset ;;; | <--> | ;;; | d2 | ;;; | MIN MAX | ;;; +-----------------------------------+ - ;;; ^^ ^ ^ ^ ^^ ^ ;;; || | | | || +-- bar-max-x ;;; || | | | |+---- (<= last-tick-x MAX-UPPER-EDGE bar-max-x) ;;; || | | | +----- last-tick-x ;;; || | | +------------- drag-max-edge ;;; || | +--------------------- drag-min-edge ;;; || +--------------------------- first-tick-x ;;; |+----------------------------- (<= bar-min-x MIN-LOWER-EDGE first-tick-x ) ;;; +------------------------------ bar-min-x ;;; ;;; Note: No margins are shown except *slider-default-margin* ;;; ;;; ;;; .--. ;;; | | max ;;; ;;; | | ;;; | | ;;; | | empty ;;; | | ;;; ;;; +----+ ;;; | | ;;; | | drag ;;; | | ;;; +----+ ;;; ;;; |**| ;;; |**| fill ;;; |**| ;;; |**| ;;; ;;; |**| min ;;; `--' ;;; (defmethod display ((slider slider) &optional at-x at-y at-width at-height &key) (with-slots (dimensions width height foreground orientation minimum maximum increment middle-length sensitive min-text-width max-text-width indicator-size font) slider ;; Default exposed rectangle, if necessary (setf at-x (or at-x 0) at-y (or at-y 0) at-width (or at-width (- width at-x)) at-height (or at-height (- height at-y))) (let* ((drag-box-width (slidebar-drag-box-width dimensions)) (drag-bar-offset (slidebar-bar-drag-offset dimensions)) (gap (slidebar-gap dimensions)) (bar-thickness (slidebar-bar-thickness dimensions)) (bar-image (getf (getf *slider-bar-images* :masks) (contact-scale slider))) (image-half-size (floor (image-width bar-image) 2)) ;; image is BOTH ends, use min half (first-tick-x (first-tick-x slider)) (last-tick-x (+ first-tick-x middle-length)) (bar-y (+ (slider-margin slider :top) *slider-default-margin* gap drag-bar-offset)) (bar-min-x (- first-tick-x ;; ZRP (slidebar-tick-mark-offset dimensions))) (bar-max-x (+ last-tick-x (slidebar-tick-mark-offset dimensions))) (drag-min-edge (drag-box-min-x slider)) (drag-max-edge (+ drag-min-edge gap drag-box-width gap)) (end-portion (min image-half-size (max 0 (- drag-min-edge bar-min-x)))) ;; for min end ONLY (min-lower-edge (+ bar-min-x end-portion)) (max-upper-edge (max (- bar-max-x image-half-size) drag-max-edge)) (mask (contact-image-mask slider bar-image :foreground foreground :background (contact-current-background-pixel slider))) (inactive-p (not (sensitive-p slider))) (scale (contact-scale slider)) ) ;; First draw the bar outline, then ;; draw the tick-marks, draw the tick-text, and ;; finally, fill the bar then blt the drag-box to the correct position. (using-gcontext (gc :drawable slider :font font :exposures :off :foreground (if inactive-p (logxor foreground (contact-current-background-pixel slider)) foreground) :fill-style (when inactive-p :stippled) :function (when inactive-p boole-xor) ;; Use 50%gray, since 25%gray looks bad (bar disappears) for args : ;; (make-slider :width 200 :height 200 :maximum 4 :orientation :vertical :scale :medium) :stipple (when inactive-p (contact-image-mask slider 50%gray :depth 1)) :clip-mask (list at-x at-y at-width at-height) ) ;; Draw MIN end - if it will be visible after drag-box is drawn later. ;; The zero reference point (ZRP) is the center of the first tick-mark. ;; If at this position we just draw the drag-box at ZRP after subtracting ;; the half-width of the drag-box to get the coordinate of the left edge. ;; Actually the image blt to the slider also contains a gap, but the ZRP is ;; situated such that it centers the drag-box at the extreme min position. ;; Draw (at least part of) MIN (when (> drag-min-edge bar-min-x) ;; drag-box is NOT less than gap away from MIN edge (multiple-value-bind (src-x src-y -width -height dst-x dst-y) (if (eq orientation :horizontal) (values 0 0 end-portion (image-width bar-image) bar-min-x bar-y) (values 0 (- (image-width bar-image) image-half-size) ;; image may have odd # of pixels (image-width bar-image) end-portion bar-y (translate-x-to-y bar-min-x end-portion slider))) (when (area-overlaps-p at-x at-y at-width at-height dst-x dst-y -width -height) (if inactive-p (draw-rectangle slider gc dst-x dst-y -width -height :fill-p) (copy-area mask gc src-x src-y -width -height slider dst-x dst-y)))) ;; Draw FILL, if any (when (> drag-min-edge min-lower-edge) (multiple-value-bind (x y -width -height) (if (eq orientation :horizontal) (values min-lower-edge bar-y (- drag-min-edge min-lower-edge) bar-thickness) (values bar-y (translate-x-to-y min-lower-edge (- drag-min-edge min-lower-edge) slider) bar-thickness (- drag-min-edge min-lower-edge))) (when (area-overlaps-p at-x at-y at-width at-height x y -width -height) (draw-rectangle slider gc x y -width -height :fill-p)))) ) ;; Draw EMPTY portion, if any (when (> max-upper-edge drag-max-edge) (multiple-value-bind (x y x2 y2 x3 y3 x4 y4) (if (eq orientation :horizontal) (values drag-max-edge bar-y ;; x y max-upper-edge bar-y ;; x2 y2 drag-max-edge (+ bar-y bar-thickness -1) ;; x3 y3 max-upper-edge (+ bar-y bar-thickness -1)) ;; x4 y4 (values bar-y (translate-x-to-y drag-max-edge 1 slider) ;; x y bar-y (translate-x-to-y max-upper-edge 1 slider) ;; x2 y2 (+ bar-y bar-thickness -1) (translate-x-to-y drag-max-edge 1 slider) ;; x3 y3 (+ bar-y bar-thickness -1) (translate-x-to-y max-upper-edge 1 slider))) ;; x4 y4 (when (area-overlaps-p at-x at-y at-width at-height x y (- x2 x) bar-thickness) (draw-segments slider gc (list x y x2 y2 x3 y3 x4 y4)))) ) ;; Draw MAX, or portion not obscured by drag-box (when (plusp (setq end-portion (min image-half-size (- bar-max-x max-upper-edge)))) (setq mask (contact-image-mask slider (getf (GETF *slider-bar-images* :borders) scale) :foreground foreground :background (contact-current-background-pixel slider))) (multiple-value-bind (src-x src-y -width -height dst-x dst-y) (if (eq orientation :horizontal) (values (- (image-width bar-image) end-portion) 0 end-portion (image-width bar-image) max-upper-edge bar-y) (values 0 0 (image-width bar-image) end-portion bar-y (translate-x-to-y max-upper-edge end-portion slider))) (when (area-overlaps-p at-x at-y at-width at-height dst-x dst-y -width -height) (if inactive-p (draw-rectangle slider gc dst-x dst-y -width -height :fill-p) (copy-area mask gc src-x src-y -width -height slider dst-x dst-y)))) ) ;; Draw TICK-TEXT, the labels for MIN and MAX (unless (eq :off indicator-size) ;; don't draw tick marks or tick text (let ((min-thickness (+ (slider-margin slider :top) (fixed-thickness slider :include-text-p nil))) (text-x-offset (first (getf (slidebar-bar-text-offset dimensions) orientation))) (text-y-offset (second (getf (slidebar-bar-text-offset dimensions) orientation)))) (multiple-value-bind (x-min y-min x-max y-max) (if (eq orientation :horizontal) (values (+ (slider-margin slider :min) text-x-offset ;; center text at :first-tick (+ (first-tick-offset slider) (- (floor min-text-width 2)))) min-thickness (- width (slider-margin slider :max) (last-tick-offset slider) (ceiling max-text-width 2)) ;; scoot left to fit on odd widths! min-thickness) (values min-thickness (+ (slider-margin slider :max) (last-tick-offset slider) middle-length (slidebar-tick-mark-offset dimensions) (- gap)) min-thickness (+ (slider-margin slider :max) (last-tick-offset slider) (- (slidebar-tick-mark-offset dimensions)) text-y-offset))) (let* ((font-ascent (max-char-ascent font)) (font-height (+ font-ascent (max-char-descent font)))) (when (if (eq orientation :horizontal) (area-overlaps-p at-x at-y at-width at-height x-min (- y-min font-ascent) (+ (- x-max x-min) max-text-width) font-height) (area-overlaps-p at-x at-y at-width at-height x-max (- y-max font-ascent) (max min-text-width max-text-width) (+ (- y-min y-max) font-height))) ;; Draw TICK-TEXT for min and max (draw-glyphs slider gc x-min y-min (format nil "~a" minimum)) (draw-glyphs slider gc x-max y-max (format nil "~a" maximum)))))) ;; Draw TICK-MARKS (multiple-value-bind (tick-limit increments-in-tick) (if (plusp indicator-size) (values (1+ (floor (- maximum minimum) (* increment indicator-size))) indicator-size) (choose-indicator-size slider)) ;; automatic tick marks (do* ((tick 0 (incf tick)) (tick-thickness (slider-tick-mark-thickness slider)) (tick-x (+ (first-tick-x slider) (- (floor tick-thickness 2))) ;; adjust from center to edge of tick (+ (first-tick-x slider) (- (floor tick-thickness 2)) (units-to-pixels slider (* tick increments-in-tick increment)))) (tick-y (+ (slider-margin slider :top) (bar-bottom-offset slider) (slider-bar-tick-gap slider))) (tick-height (slidebar-tick-mark-length dimensions))) ((= tick tick-limit)) ;; draw tick @min plus TICK-LIMIT more (multiple-value-bind (x y -width -height) (if (eq orientation :horizontal) (values tick-x tick-y tick-thickness tick-height) (values tick-y (translate-x-to-y tick-x tick-thickness slider) tick-height tick-thickness)) (when (area-overlaps-p at-x at-y at-width at-height x y -width -height) (draw-rectangle slider gc x y -width -height :fill-p) )))) ) ;; Draw DRAG BOX (possibly over a tick mark) (let ((drag-image (getf (getf *slider-drag-box-images* orientation) scale))) (setq mask (contact-image-mask slider drag-image :foreground foreground :background (contact-current-background-pixel slider))) (multiple-value-bind (src-x src-y -width -height dst-x dst-y) (if (eq orientation :horizontal) (values 0 0 (image-width drag-image) (image-height drag-image) drag-min-edge (- bar-y drag-bar-offset gap)) (values 0 0 (image-width drag-image) (image-height drag-image) (- bar-y drag-bar-offset gap) (translate-x-to-y drag-min-edge (image-height drag-image) slider))) (when (area-overlaps-p at-x at-y at-width at-height dst-x dst-y -width -height) (if inactive-p (draw-rectangle slider gc dst-x dst-y -width -height :fill-p) (copy-area mask gc src-x src-y -width -height slider dst-x dst-y)) (when (boundp '*highlight-pixel*) (special-highlite-drag-box slider gc dst-x dst-y -width -height gap))))))))) ;;; Crock! This function could be inlined, except that causes the Explorer compiler ;;; to barf on (method display (slider)) when using R4 CLX. (defun special-highlite-drag-box (slider gc dst-x dst-y -width -height gap) (declare (special *highlight-pixel*)) (with-gcontext (gc :function boole-xor :foreground *highlight-pixel*) ;; Highlight drag area while button is still down ) (highlite-drag-box slider gc dst-x dst-y -width -height gap)))