.. include:: /include/substitutions.txt .. include:: /include/external_links.txt .. include:: /include/custom_roles.txt .. _viewport: ******** Viewport ******** What Is a View's Viewport? ************************** Like the CSS Viewport (which is the visible area of a web page), a Sublime |nbsp| Text :term:`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. .. figure:: _static/images/viewport_edges.png :align: center :scale: 80% :alt: Image showing left and right edges of Viewport :name: viewport_figure Left and Right Edges of Viewport The ``sublime.View`` class has a set of methods involving the :term:`View's ` Viewport, which I will simply call the "Viewport API". Viewport Terminology ******************** From ``sublime_types.py`` (lines are omitted that are not relevant): .. code-block:: py 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 |nbsp| Text. A View's "Layout" is the entire rectangular area of existing text in a View's :term:`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 :term:`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 |nbsp| Text. 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. .. code-block:: py 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) Example ******* Problem: given a :term:`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. .. code-block:: py >>> _, 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". .. code-block:: py >>> # 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. # -------------------------------------------------------------------------