27. Viewport

27.1. What Is a View’s Viewport?

Like the CSS Viewport (which is the visible area of a web page), a Sublime Text View has a Viewport, which is the visible area of editable text, between the left and right gutters and above any horizontal scrollbar at the bottom of the View.

Note that the inside edge of the right gutter shares the same background color as the Viewport, so to make it visible, edit a file type without word wrapping and make a line longer than the width of the viewport and a shadow will appear showing the edge of the right gutter.

Image showing left and right edges of Viewport

Fig. 27.1.1 Left and Right Edges of Viewport

The sublime.View class has a set of methods involving the View’s Viewport, which I will simply call the “Viewport API”.

27.2. Viewport Terminology

From sublime_types.py (lines are omitted that are not relevant):

from typing_extensions import TypeAlias

DIP: TypeAlias = 'float'
Vector: TypeAlias = 'Tuple[DIP, DIP]'
Point: TypeAlias = 'int'

Of interest to our learning about the Viewport API are types:

DIP

TypeAlias = ‘float’ containing a distance/length measurement. DIP stands for device- or density-independent pixels, a unit of length based on a “reference screen” having 160 DPI. Thus, 1 DIP = 1/160 inch = 0.00625 inch.

Vector

TypeAlias = ‘Tuple[DIP, DIP]’ = (xf, yf)

Point

TypeAlias = ‘int’ containing a zero-based character offset within a View’s Buffer.

Additionally, there are 4 more terms you will need to understand related to the Viewport API:

Layout

The term “Layout” is used in this context because the Viewport API has several methods containing the term “Layout”, so you need to know what it means, because it is unique to Sublime Text.

A View’s “Layout” is the entire rectangular area of existing text in a View’s Buffer, rendered in the current font and font size, whether it is visible or not.

The Layout’s dimensions are exactly the DIP width and height of the text itself, after being rendered with the current font in the current font size.

If the amount of text in a View is smaller in width and height than the Viewport, then the Layout’s dimensions are smaller than the Viewport, since it is only the DIP width and height of the text itself. If the View’s vertical scroll bar is scrolled all the way to the top, then this means that all the text in the Layout will be visible in the Viewport.

As the text grows in length and width, so do the Layout’s dimensions. When the width of the text (and thus the Layout’s width) grows wider than the width of the Viewport, a horizontal scroll bar appears at the bottom of the View. (Incidentally, the appearance of the horizontal scrollbar causes causing the Viewport’s height to shorten by the height of the scrollbar, typically 15 DIP.) No matter how large the text is vertically or horizontally, the dimensions of the Layout are the dimensions of the rendered text itself, and these can be larger or smaller than the dimensions of the Viewport. When they are larger in either direction, the Viewport is a “window” into the Layout.

Layout Coordinates

(x,y) coordinates in DIP units of a point in the rendered text of a View’s Buffer. These are used to translate a “position in the Layout” to a particular point in the text in the View’s Buffer, or vice versa. Point (0,0) is the upper-left-most corner of the first character in the Buffer, and positive Y proceeds downward.

Client Area

The Client Area of a window is a Windows OS term and it means the portion of an application’s window below the Main Menu (if one is present) and above the status bar (if one is present). This is generally considered the “production area” of the application.

Window Coordinates

(x,y) coordinates in DIP units of a point in the window’s “Client Area”. These are used to translate a “position in the window” (e.g. from a mouse cursor) to a particular point in the text in the View’s Buffer, or vice versa. Point (0,0) is the upper-left corner of the window’s “Client Area” (about 4 DIP below and about 7 DIP to the left of the “F” in the “File” menu). Positive Y proceeds downward. This term is specific to Sublime Text.

27.3. Enhanced Viewport API

Now that we have defined the needed terms, the below are the API methods from class View, but with the comments rewritten so the use of each method is easier to understand.

def viewport_position(self) -> Vector:
    """
    :returns:   Scroll offset of Viewport in Layout coordinates:
                an ``(x_float, y_float)`` tuple that indicates the
                *scroll position* down, and to the right, of the
                upper-left corner of the View's text (i.e. its Layout)
                in DIP units:

                - x_float = DIPs viewport is scrolled to the right, and
                - y_float = DIPs viewport is scrolled down.
    """
    return sublime_api.view_viewport_position(self.view_id)

def set_viewport_position(self, xy: Vector, animate=True):
    """
    Scroll Viewport to Layout position ``xy``.

    :param xy:  An ``(x_float, y_float)`` tuple of the same type as is
                returned by ``view.viewport_position()``.  If given a tuple
                returned by ``view.viewport_position()``, it will return the
                viewport's scroll position to exactly where it was when
                ``view.viewport_position()`` was called.
    """
    sublime_api.view_set_viewport_position(self.view_id, xy, animate)

def viewport_extent(self) -> Vector:
    """
    :returns:   ``(width_float, height_float)`` tuple containing current Viewport
                dimensions in DIP units.  This is the editing area between the left
                and right gutters and above the horizontal scrollbar (if visible)
                at the bottom of the View.
    """
    return sublime_api.view_ _extents(self.view_id)

def layout_extent(self) -> Vector:
    """
    :returns:   ``(width_float, height_float)`` tuple containing width and height of
                the Layout (text), rendered with current font and font size.  These
                dimensions can be larger or smaller than those of the Viewport.
    """
    return sublime_api.view_layout_extents(self.view_id)

def text_to_layout(self, tp: Point) -> Vector:
    """
    :param tp:  Point in text to translate.

    :returns:   ``(x_float, y_float)`` tuple that indicates the position of ``tp``
                point in the View's Buffer translated to Layout Coordinates.
                If ``tp`` is negative, then (0.0, 0.0) is returned.  If ``tp``
                is beyond the end of the Buffer, it is clamped to the last Point
                in the Buffer.
    """
    return sublime_api.view_text_to_layout(self.view_id, tp)

def layout_to_text(self, xy: Vector) -> Point:
    """
    :param xy:  ``(x_float, y_float)`` tuple giving an (x,y) DIP-coordinate
                position within the Layout.  This is the same type of tuple as is
                returned from ``text_to_layout()``.

    :returns:   Text Point in View's Buffer closest to ``xy``.  This is meant
                to connect an (x,y) position within the Layout to a point within
                the View's Buffer.  This is the reverse of ``text_to_layout()``.

                If ``x_float`` is to the right of the end of a line, it is
                clamped to the end of that row of text.

                If ``x_float`` is negative, it is clamped to the beginning of
                that row of text.

                If ``y_float`` is below (beyond) the bottom of the Layout, it is
                clamped to the bottom of the Layout.

                If ``y_float`` is negative, it is clamped to 0 (the beginning of
                the Layout.
    """
    return sublime_api.view_layout_to_text(self.view_id, xy)

def text_to_window(self, tp: Point) -> Vector:
    """
    :param tp:  Point in text to translate.

    :returns:   ``(x_float, y_float)`` tuple that indicates the position of ``tp``
                within the View's Buffer translated to Window Coordinates.  If
                that precise point is not visible in the View's Viewport, even
                by 1 pixel, then (0.0, 0.0) is returned.
    """
    return self.layout_to_window(self.text_to_layout(tp))

def window_to_text(self, xy: Vector) -> Point:
    """
    :param xy:  ``(x_float, y_float)`` tuple giving an (x,y) DIP-coordinate
                position within the Window.  This is the same type of tuple as is
                returned from ``text_to_window()``.

    :returns:   Text Point in View's Buffer closest to specified Window
                coordinates.  This is the reverse of ``text_to_window()``.
                See ``text_to_window()`` for details.

                If either ``x_float`` or ``y_float`` are beyond the boundaries of
                the Layout, they are clamped to a range so as to return a valid
                Point within the Buffer.  When ``x_float`` is out of range, the
                Point returned is on the same row of text as though ``x_float``
                was at the closest valid point on that row of text.
    """
    return self.layout_to_text(self.window_to_layout(xy))

def layout_to_window(self, xy: Vector) -> Vector:
    """
    :param xy:  ``(x_float, y_float)`` tuple giving an (x,y) DIP-coordinate
                position within the Layout.  This is the same type of tuple as is
                returned from ``text_to_layout()``.

    :returns:   ``(x_float, y_float)`` tuple giving ``xy`` translated to
                Window Coordinates.

                If ``xy`` is anywhere outside the Window's (Client Area)
                boundaries, based on the scrolled position of the layout,
                then (0.0, 0.0) is returned.  In other words, ``xy`` can go
                below the Layout (text) so long as the vertical scrolling
                positions that point within the Window's boundaries.  Once
                it goes beyond those boundaries, (0.0, 0.0) is returned.

    """
    return sublime_api.view_layout_to_window(self.view_id, xy)

def window_to_layout(self, xy: Vector) -> Vector:
    """
    :param xy:  ``(x_float, y_float)`` tuple giving an (x,y) DIP-coordinate
                position within the Window.  This is the same type of tuple as is
                returned from ``text_to_window()``.

    :returns:   ``(x_float, y_float)`` tuple giving ``xy`` translated to Layout
                Coordinates based on the currently-scrolled position of the text.
                This is the reverse of ``layout_to_window()`` except that no
                clamping of the values in ``xy`` is done:  whatever coordinates
                arrive in ``xy``, they are translated to "relative coordinates"
                in the Layout, even if they are outside the Layout's boundaries.
    """
    return sublime_api.view_window_to_layout(self.view_id, xy)

def line_height(self) -> DIP:
    """
    :returns:   Line height of current font in current font size in DIP units.
    """
    return sublime_api.view_line_height(self.view_id)

def em_width(self) -> DIP:
    """
    :returns:   Typical character width of 1 character of current font in current
                font size in DIP units.
    """
    return sublime_api.view_em_width(self.view_id)

27.4. Example

Problem: given a Point pt, how do we compute what percentage of the way down it is in the Viewport, for restoring that scroll position later? We will call this “scroll position in Viewport”.

Given: pt is the caret Point that we want to remember the “scroll position in Viewport” for.

>>> _, dip_viewport_is_scrolled_down = view.viewport_position()
>>> dip_viewport_is_scrolled_down
1665.0
>>> _, dip_pt_down_from_layout_top = view.text_to_layout(pt)
>>> dip_pt_down_from_layout_top
1755.0
>>> dip_pt_from_top_of_viewport = dip_pt_down_from_layout_top - dip_viewport_is_scrolled_down
>>> dip_pt_from_top_of_viewport
90.0
>>> _, viewport_height = view.viewport_extent()
>>> viewport_height
1381.0
>>> pct_down_from_viewport_top = dip_pt_from_top_of_viewport / viewport_height
>>> pct_down_from_viewport_top
0.06517
# -------------------------------------------------------------------------
# 6.5% down from the top of the Viewport.
# And so we save `pct_down_from_viewport_top` in the Marker.
# -------------------------------------------------------------------------

Now given a percent of the way down in the Viewport, this is how we scroll the Viewport vertically to place the restored caret at that same “scroll position”. This is done mostly in reverse of how the percent was computed.

Given: rgn is the region retrieved from the View’s Region Dictionary containing the caret position being “restored”.

>>> # Retrieve `pct_down_from_viewport_top` from Marker.
>>> pct_down_from_viewport_top = marker[_pct_fr_top_key]
>>> pct_down_from_viewport_top
0.06517
>>> _, viewport_height = view.viewport_extent()
>>> viewport_height
1381.0
# -------------------------------------------------------------------------
# The Viewport's height can be different from original height, but we use
# original value for illustration.
# -------------------------------------------------------------------------
>>> dip_pt_from_top_of_viewport = viewport_height * pct_down_from_viewport_top
>>> dip_pt_from_top_of_viewport
90.0
>>> pt = rgn.b
>>> pt
# -------------------------------------------------------------------------
# Whatever ``pt`` was---it can be different now if text was added or
# removed above the marker.
# -------------------------------------------------------------------------
>>> _, dip_pt_down_from_layout_top = view.text_to_layout(pt)
>>> dip_pt_down_from_layout_top
1755.0
>>> dip_viewport_is_scrolled_down = dip_pt_down_from_layout_top - dip_pt_from_top_of_viewport
>>> dip_viewport_is_scrolled_down
1665.0
>>> view.set_viewport_position((0.0, dip_viewport_is_scrolled_down), animate=_animate_scroll)
# -------------------------------------------------------------------------
# This scrolls the Viewport to correct position, and relative % position
# within the Viewport is preserved.
# -------------------------------------------------------------------------