TIP 438: Ensure Line Metrics are Up-to-Date

Login
Bounty program for improvements to Tcl and certain Tcl packages.
Author:         François Vogel <fvogelnew1@free.fr>
Author:         Jan Nijtmans <jan.nijtmans@gmail.com>
State:          Final
Type:           Project
Vote:           Done
Created:        01-Nov-2015
Post-History:   
Keywords:       Tk,text
Tcl-Version:    8.6.5
Tk-Branch:      tip-438

Abstract

The text widget calculates line metrics asynchronously, for performance reasons. Because of this, some commands of the text widget may return wrong results if the asynchronous calculations are not over. This TIP is about providing the user with ways to ensure that line metrics are up-to-date.

Rationale

The text widget features asynchronous calculation of the display height of logical lines. The reasons for this and the details of the implementation are explained at the beginning of tkTextDisp.c.

This approach has definite advantages among which responsivity of the text widget is important. Yet, there are drawbacks in the fact the calculation is asynchronous. Some commands of the text widget may return wrong results if the asynchronous calculations are not finished at the time these commands are called. For example this is the case of .text count -ypixels, which was solved by adding a modifier -update allowing the user to be sure any possible out of date line height information is recalculated.

It appears that aside of .text count -ypixels there are several other cases where wrong results can be produced by text widget commands. These cases are illustrated in several bug reports:

In all these cases, forcing the update by calling .text count -update -ypixels 1.0 end before calling .text yview, or .text yview moveto solves the issue presented in the ticket. This has however a performance cost, of course, but the above tickets show that there are cases where the programmer needs accurate results, be it at the cost of the time needed to get the line heights calculations up-to-date.

This TIP is about providing the user/programmer with (better) ways to ensure that line metrics are up-to-date.

Indeed it is not appropriate to let the concerned commands always force update of the line metrics or wait for the end of the update calculation each time they are called: performance impact would be way too large.

Also, it has to be noted that the update command is of no help here since the line metrics calculation is done within the event loop in a chained sequence of [after 1] handlers.

Proposed Change

It is proposed to add two new commands to the text widget:

pathName sync ?-command command?

pathName pendingsync

Also a new virtual event <<WidgetViewSync>> will be added.

Description:

pathName sync

Immediately brings the line metrics up-to-date by forcing computation of
any outdated line pixel heights. Indeed, to maintain a responsive
user-experience, the text widget caches line heights and re-calculates them
in the background. The command returns immediately if there is no such
outdated line heights, otherwise it returns only at the end of the
computation. The command returns an empty string.

Implementation details: The command executes:

    TkTextUpdateLineMetrics(textPtr, 1,
	      TkBTreeNumLines(textPtr->sharedTextPtr->tree, textPtr), -1);

pathName sync -command command

Schedule _command_ to be executed exactly once as soon as all line
calculations are up-to-date. If there are no pending line metrics
calculations, the scheduling is immediate. The command returns the empty
string. **bgerror** is called on _command_ failure.

pathName pendingsync

Returns 1 if the line calculations are not up-to-date, 0 otherwise.

<<WidgetViewSync>>

A widget can have a period of time during which the internal data model is
not in sync with the view. The **sync** method forces the view to be in
sync with the data. The **<<WidgetViewSync>>** virtual event fires when
the internal data model starts to be out of sync with the widget view, and
also when it becomes again in sync with the widget view. For the text
widget, it fires when line metrics become outdated, and when they are
up-to-date again. Note that this means it fires in particular when
_pathName_ **sync** returns \(if there was pending updates\). The detail
field \(%d substitution\) is either true \(when the widget is in sync\) or
false \(when it is not\).

All sync, pendingsync and <<WidgetViewSync>> apply to each text widget independently of its peers.

The names sync, pendingsync and <<WidgetViewSync>> are chosen because of the potential for generalization to other widgets they have.

The text widget documentation will be augmented by a short section describing the asynchronous update of line metrics, the reasons for that background update, the drawbacks regarding possibly wrong results in .text yview or .text yview moveto, and the way to solve these issues by using the new commands. Example code as below will be provided in the documentation, since this code will not be included in the library (i.e. in text.tcl)).

The existing -update modifier switch of .text count will become obsolete. It will be declared as deprecated in the text widget documentation page while being still supported for backwards compatibility reasons.

Using the new commands, ways to ensure accurate results in .text yview, or .text yview moveto are as in the following example:

    ## Example 1:

    # runtime, immediately complete line metrics at any cost (GUI unresponsive)
    $w sync
    $w yview moveto $fraction

    ## Example 2:

    # runtime, synchronously wait for up-to-date line metrics (GUI responsive)
    $w sync -command [list $w yview moveto $fraction]

    ## Example 3:

    # init
    set yud($w) 0
    proc updateaction w {
        set ::yud($w) 1
        # any other update action here...
    }

    # runtime, synchronously wait for up-to-date line metrics (GUI responsive)
    $w sync -command [list updateaction $w]
    vwait yud($w)
    $w yview moveto $fraction

    ## Example 4:

    # init
    set todo($w) {}
    proc updateaction w {
        foreach cmd $::todo($w) {uplevel #0 $cmd}
        set todo($w) {}
    }

    # runtime
    lappend todo($w) [list $w yview moveto $fraction]
    $w sync -command [list updateaction $w]

    ## Example 5:

    # init
    set todo($w) {}

    bind $w <<WidgetViewSync>> {
        if {%d} {
            foreach cmd $todo(%W) {eval $cmd}
            set todo(%W) {}
        }
    }

    # runtime
    if {![$w pendingsync]} {
        $w yview moveto $fraction
    } else {
        lappend todo($w) [list $w yview moveto $fraction]
    }

Rejected alternatives

  • Use a script-visible array variable such as ::tk::metricsDone($w) instead of an event.

  • Don't change the source code and better document the .text count -update -ypixels trick. This is believed to be suboptimal considering that .text count indeed performs counting (which has a cost). This performance drawback could however be very much alleviated by counting between the two same indices: there would be no cost at all if this case was detected and was a short-cut in function TextWidgetObjCmd.

  • Instead of a new text widget sub-command, follow the lines of the existing example of text count and provide a new modifier switch -update to all sub-commands that may need it. The list of such sub-commands include text yview, text yview moveto, and text yview scroll.

  • update idletasks could force line metrics calculation update (in addition to what this command already does). This is certainly not the right thing to do since it is not very flexible. It would impact the performance of all text widgets whereas perhaps only one of them needs up-to-date line heights. Also, one could want to update idletasks (in the current sense: idle tasks) but not the line heights calculation, or the opposite. All in all, linking the event loop and the line heights calculation seems bad.

  • For each sub-command that needs up-to-date line heights to provide fully correct results, detect whether it is the case or not at the time they are called. If so, fine. If not, there could be two ways forward:

    1. Force the update. This is not believed to be desirable, again for performance reasons. While there are cases where accurate results are mandatory (see the tickets above), most of the time one can live with approximate results. Any mismatch is temporary, since the asynchronous line height calculations will always catch up eventually. It is preferred to let the programmer decide if this update is needed or not.

    2. Decide that the line height of not yet up-to-date lines is equal to some reasonable value, for instance the height of the first displayed line (which is likely up-to-date). For text widgets using only a single font, this would be OK since all line heights are then the same. However this would not solve all cases, for instance in text yview where the total number of pixels used by the text widget contents is needed, because this total pixel height calculation involves the total number of display (not logical) lines. Assessing the total number of display lines has a performance cost similar to proper line heights calculation, which voids that path.

  • It has been proposed that the detail field %d for the <<WidgetViewSync>> event contain the number of outdated lines, while this event would fire at each [after 1] partial update of the line metrics. This was rejected since no use case of this value could be exhibited, and it was believed that firing the event twice (when out of sync and when again in sync) was sufficient.

  • It has been proposed that the text pendingsync command return the number of currently outdated lines. This was rejected because no use case could be found, and because this TIP aims at generalization and it might be hard to define the equivalent of "number of lines to do" for other widgets. Anyway, using a boolean now (noted as "1" and "0", rather than "true" and "false") leaves room to change our minds later with minimal incompatibility, since [if {[.t pendingsync]}] will keep its semantics with an integer.

Copyright

This document has been placed in the public domain.

History