All Manuals > CAPI User Guide and Reference Manual > 12 Creating Panes with Your Own Drawing and Input

12.4 output-pane scrolling

An output-pane or an instance of any of its subclasses can be made to scroll by passing the :vertical-scroll and/or :horizontal-scroll initargs which are inherited from simple-pane.

12.4.1 Ordinary scrolling

By default, the scrolling is what is called ordinary scrolling. In this case you just need to specify that you want scrolling by :vertical-scroll and/or :horizontal-scroll, and maybe also specify the internal scroll dimension(s) (see below).

In ordinary scrolling, all the interactions are done as if the pane has an "internal canvas" with dimensions (the "internal dimensions") which are different from the visible dimensions on the screen, and typically larger. The coordinates of input gestures and drawing in the pane are all with respect to this internal canvas. Only part of the canvas is displayed at any one time, depending on the position of the scroll slugs. The effect of scrolling is to change what part of the pane is visible, which causes a display-callback to draw any newly visible areas. However, the call to the display-callback is an ordinary call like any call (for example, like a call as result of part of the window being exposed), and the display-callback does not need to know anything about scrolling.

If you need to know when scrolling happened, rather than just display what is needed to display, you can use the :scroll-callback initarg to specify a callback that is called before the display-callback. However, this is not required for ordinary scrolling to work.

The internal dimensions of the pane can be specified by the initargs :scroll-height and :scroll-width, and can also be set dynamically set by set-vertical-scroll-parameters and set-horizontal-scroll-parameters. Some subclasses can compute their internal dimensions, for example graph-pane computes its internal dimensions to show all the graph, and static-layout and its subclass pinboard-layout by default compute the internal dimensions to fit their children (unless fit-size-to-children is nil).

For example, create an output-pane with vertical scroll and internal height of 600 pixels, minimum visible height of 300 pixels, and a display-callback that prints the y coordinate and the height and displays a green square at (0,100) of size 10x10 and a blue square at (0,400) of size 10x10:

(defun my-display-callback (pane x y width height)
  (declare (ignore x width))
  (format t " y = ~d,  height =  ~d~%" y height)
  (gp:draw-rectangle pane 0 100 10 10
                     :foreground :green :filled t)
  (gp:draw-rectangle pane 0 400 10 10
                     :foreground :blue :filled t))
 
(setq output-pane
      (make-instance 'capi:output-pane
                      :vertical-scroll t
                      :scroll-height 600
                      :visible-min-height 300
                      :display-callback 'my-display-callback))

Then display it:

(capi:contain output-pane)

When it appears on the screen its height is 300 pixels, the scrollbar is half the height. You receive a display callback with y being 0 and height 300. You see the green square 100 pixels down from the top. The blue square is invisible, because it is drawn at y = 400, which is not inside the visible area.

Now if you scroll to the bottom, you will receive a callback with y = 300 and height still 300 (possibly after several callbacks with intermediate y values). Now you see the blue square 100 pixels from the top, and the green square is invisible.

Note that the display callback knows nothing about the scrolling. It just draws. A real display callback may be made faster by avoiding the drawings which are not going to be visible, for example:

(defun my-display-callback-1 (pane x y width height)
  (declare (ignore x width))
  (format t " y = ~d,  height =  ~d~%" y height)
  (unless (or (> y 110) (< (+ Y height) 100) (> x 10))
    (gp:draw-rectangle pane 0 100 10 10
                       :foreground :green :filled t))
  (unless (or (> y 410) (< (+ Y height) 400) (> x 10))
     (gp:draw-rectangle pane 0 400 10 10
                        :foreground :blue :filled t)))

but this is just optimization. It does not affect what is shown on the screen.

12.4.2 Internal scrolling

The other type of scrolling is called internal scrolling (sometimes "pane scrolling"), and it is set up by passing the output-pane initarg :coordinate-origin with either :fixed or :fixed-graphics. In general, internal scrolling is more complex to use, but allows more flexible scrolling.

When using internal scrolling with coordinate-origin :fixed, drawing coordinates are relative to the visible area, and the coordinates arguments to callbacks are also relative to the visible area. Thus drawing a rectangle at 0,100 as my-display-callback above does will always show it at 0,100 on the screen, ignoring any scrolling.

For example, evaluate the following (which requires the definition of my-display-callback):

 
(capi:contain (make-instance
               'capi:output-pane 
               :vertical-scroll t
               :scroll-height 600 
               :visible-min-height 300
               :display-callback 'my-display-callback
               :coordinate-origin :fixed  ; <<
               )
              :title "With :coordinate-origin :fixed")

Scroll it and you will see that it is "fixed": the green rectangle does not move, and the y coordinate that is passed to my-display-callback is always 0.

When using internal scrolling with coordinate-origin :fixed-graphics, the drawing coordinate are relative to the visible pane, but CAPI coordinates (that is the arguments to callbacks such as display-callback, scroll-callback and input-model and in calls to display-popup-menu) are offset by the scroll position of the pane like in ordinary scrolling. The scroll position can be obtained by calling get-horizontal-scroll-parameters and get-vertical-scroll-parameters with :slug-position, or from %scroll-x% and %scroll-y% inside with-geometry.

For example, evaluate this:

(capi:contain (make-instance
               'capi:output-pane 
               :vertical-scroll t
               :scroll-height 600 
               :visible-min-height 300
               :display-callback 'my-display-callback
               :coordinate-origin :fixed-graphics ;<<
               )
              :title "With :coordinate-origin :fixed-graphics")

Scroll it and you will see that the graphics are "fixed" (the green rectangle does not move) but the coordinates "scroll" (the y coordinate increases as you scroll). In practice, this means that to get the effect of scrolling, the display-callback needs to subtract the scroll position before drawing, or use Graphics Ports transformations, for example:

(gp:with-graphics-translation (pane (- scroll-x) (- scroll-y))
   (do-all-the-drawing))

If you do not supply scroll-callback (inherited from simple-pane) in a pane that does internal scrolling, then LispWorks calls update-internal-scroll-parameters in response to scrolling gestures to update the internal parameters (that updates the scroll bars themselves if needed), and then calls invalidate-rectangle, which will cause the display-callback to be called for the whole visible area of the pane. In many cases, that is what you need, but not always.

In some cases, redisplaying the whole of the pane every time it scrolls may not be required or may be too slow, and in other cases you will want to do other things. In these situations, performs the scrolling yourself by supplying a scroll-callback. When you supply a scroll-callback, your function is responsible for doing anything that needs to be done to make "scrolling" happen (which is not necessarily proper scrolling).

In general, your scroll-callback will have to call update-internal-scroll-parameters (and maybe set-vertical-scroll-parameters or set-horizontal-scroll-parameters) to update the scroll parameters, and get-vertical-scroll-parameters and get-horizontal-scroll-parameters to get the scroll values. Some of these values may be initialized by the :scroll-... initargs of output-pane. scroll-callback may also need to do other computations.

Once the scroll-callback has adjusted the internal scrolling state of the application, it needs to ensure that the pane is redisplayed, by calling invalidate-rectangle on the area (or on each of multiple areas) that need(s) to be redisplayed. This will then cause the display-callback of the output-pane to be called on those areas. The display-callback needs to know how to draw the pane taking into account the internal scrolling state. It can do that by calling get-vertical-scroll-parameters and get-horizontal-scroll-parameters (or using the %scroll-...% variables inside with-geometry), or by using some internal scrolling state that scroll-callback has set up.

For examples of internal scrolling that do a little unconventional scrolling see:

(example-edit-file "capi/output-panes/coordinate-origin-fixed")

For an example of internal scrolling that does something different altogether (rotating) see:

(example-edit-file "capi/output-panes/fixed-origin-scrolling")

Ordinary scrolling is not only easier to use, but is also normally more efficient, because the underlying window system handles scrolling. In particular, areas that move on the screen are just copied, without a need to redraw what is displayed.

Internal scrolling is useful in situations where what is displayed changes according to the scroll position, other than just scrolling. With ordinary scrolling, the underlying window system calls the display-callback when scrolling happens, but only for areas that become visible by the scroll operation. Other areas are normally just copied to their new locations, so the program cannot change them. For example, the display callback below tries to keep a string with a yellow background at a fixed position 100 pixels down from the top left of the pane:

(defun a-display-callback (pane x y width height)
  (let* ((scroll-y 
          (capi:get-vertical-scroll-parameters pane 
              :slug-position)))
    (gp:draw-string pane "A string"  0 (+ scroll-y 100)
                    :background :yellow :block t)))
 
(capi:contain 
 (make-instance 'capi:output-pane
                :vertical-scroll t
                :scroll-height 900
                :visible-max-height 600
                :display-callback 'a-display-callback))

However, once you display it and try to scroll, it should be obvious that it does not work because the window system moves the string an the display callback is not called for the area 100 pixels down from the top left of the pane.

One way of working around this kind of issue is add a scroll-callback that fixes the display, for example by calling invalidate-rectangle, but that can become quite complex. The other way is to use internal scrolling.

Apart from the display-callback, the scroll-callback and any code that needs to know about scrolling because of the logic of the application, the rest of your code should not need to worry about scrolling. Thus it does not actually add must complexity to your code.

Another situation when you may prefer internal scrolling is when your code precomputes what to display based on the scroll position, and the display-callback does minimal computation that is not substantially more expensive than the copying the system would do. That will mean that the display-callback does not need to know about scrolling, but all your callbacks will either have to add the scroll position to the their arguments, or work with respect to the precomputed information rather than the whole pane. The latter is what editor-pane does.


CAPI User Guide and Reference Manual (Windows version) - 01 Dec 2021 19:33:53