TIP #438: ENSURE LINE METRICS ARE UP-TO-DATE ============================================== Version: $Revision: 1.16 $ Author: François Vogel Jan Nijtmans State: Final Type: Project Tcl-Version: 8.6.5 Vote: Done Created: Sunday, 01 November 2015 URL: https://tip.tcl-lang.org438.html Post-History: ------------------------------------------------------------------------- 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: * [] (.text yview moveto) * [] (.text yview) 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 *<>* 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. *<>* 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 *<>* 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 *<>* apply to each text widget independently of its peers. The names *sync*, *pendingsync* and *<>* 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 <> { 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 *<>* 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. ------------------------------------------------------------------------- TIP AutoGenerator - written by Donal K. Fellows