Scroll a text widget to display a line at a specific place

MG 2025-13-05 The text widget's see subcommand makes a particular index visible on the screen, but you have no control over where it becomes visible - it may be placed at the top, middle, or bottom of the widget display, depending on how far away it is relative to the current display, and if it's already displayed somewhere (in full), it won't scroll at all.

The proc below allows you to scroll a text widget so that a specific index is displayed at your choice of the top, center, or bottom of the display for the widget.

Usage:

scrollTextWidget $widget $index [-place top|center|bottom] [-skipvisible boolean]

  $widget - the text widget to scroll
  $index - index to display - accepts anything [$widget index] does
  -place - where in the visible display to place the index; defaults to "top"
  -skipvisible - skip scrolling if the index is already (at least partially) visible? Defaults to false

Issues:

I can't find any way to tell if an index is fully displayed. There's definitely internals for this, as [$textWidget see\] will move if it's partially visible, but I can't find any text-widget subcommand for this. [$textWidget bbox] helps for the top line, as the 2nd value will be negative, but I can't find anything that's useful if it's the bottom line - so, ironically, after scrolling manually, the code then uses see to ensure it's fully on-screen (at that point, it won't impact position as it's close enough to top/center/bottom it won't shift it beyond there).

I always use text widgets with -wrap word; I've tested this very briefly with -wrap none and an index that wasn't at the start of a line, and that seemed to work OK (mainly due to using [$widget see\] which handles the horizontal scrolling to ensure the specific index can be seen on it's line), but as that's not within my usage, I have only lightly tested it - and I make no guarantees about horizontal placement for the index, only vertical.

Code:

#: proc scrollTextTo
#: arg win Text widget to scroll
#: arg index Index to make visible
#: args -place (top, center, bottom) - where to scroll the line to
#: args -skipvisible (boolean) - if the line is already visible on screen, skip scrolling?
#: desc Scroll a text widget so a particular index is visible. If the -place arg is given,
#: desc scroll the widget to display the index at the specified position in the display.
#: desc If -skipvisible is true, won't scroll of the index is already visible.
#: desc Unlikely to work well with embedded images or windows as they will skew line heights.
#: return 0 for an error, 1 if successfully scrolled
proc scrollTextTo {win index args} {
        
        # Check window is valid
        if { ![winfo exists $win] } {
                return 0;
        }
        
        # Check index is valid
        if { [catch {$win index $index} index] } {
                return 0;
        }
        
        # Parse arguments
        
        # Set default options for args
        set opt(-place) "top"
        set opt(-skipvisible) "0"
        
        # Check each arg has a name and value
        if { ([llength $args] % 2) } {
                return 0;
        } elseif { [llength $args] } {
                # Validate arguments
        
                # Check each -arg is valid
                foreach {x y} $args {
                        if { ![info exists opt([set x [string tolower $x]])] } {
                                set matches [lsearch -all -unique -glob -inline [array names opt] "$x*"]
                                if { [llength $matches] != 1 } {
                                        # No exact match or single glob match
                                        return 0;
                                }
                                set x [lindex $matches 0]
                        }
                        # Set value
                        set useropt($x) $y
                }
        
                if { [info exists useropt(-place)] } {
                        # Validate value of -place arg
                        set places [list top center bottom]
                        set placesC [list centre middle]
                        if { [llength [set p [lsearch -exact -nocase -inline $places $useropt(-place)]]] } {
                                set opt(-place) $p
                        } elseif { [llength [set p [lsearch -exact -nocase -inline $placesC $useropt(-place)]]] } {
                                set opt(-place) "center"
                        } else {
                                return 0;
                        }
                }
                if { [info exists useropt(-skipvisible)] } {
                        # Validate value of -skipvisible arg
                        if { ![string is boolean -strict $useropt(-skipvisible)] } {
                                return 0;
                        } else {
                                set opt(-skipvisible) $useropt(-skipvisible)
                        }
                }
        }
        
        if { $opt(-skipvisible) && [llength [$win bbox $index]] } {
                # Index is already (at least partially) visible on screen; skip as instructed
                return 1;
        }
        
        # Find where we need to scroll to
        set indexes(top) [$win index @0,0]
        set indexes(bottom) [$win index "@[winfo width $win],[winfo height $win] linestart"]
        # Count display lines betwen top and bottom - this may be slightly off if the widget is not exactly X lines high
        set count [$win count -displaylines $indexes(top) $indexes(bottom)]
        set fdiff [expr {$count/2.0}]
        set diff [expr {round($fdiff)}]
        if { $diff > 0 && round($fdiff) > floor($fdiff) } {
                incr diff -1
        }
        # Calculate centre displayed line
        set indexes(center) [$win index "$indexes(top) + $diff display lines"]
        set diff [$win count -displaylines $indexes($opt(-place)) $index]
        $win yview scroll $diff units
        $win see $index;# ensures index is fully visible on screen
        
        return 1;
};# scrollTextTo

Example:

pack [text .t -wrap word] -side left -expand 1 -fill both -anchor nw
pack [scrollbar .sy -orient vertical] -side left -fill y -anchor e
.t config -yscroll [list .sy set]
.sy config -command [list .t yview]
for {set i 1} {$i < 100} {incr i} {
  .t insert end "This is line $i\n"
}

raise .

after 1000 [list scrollTextTo .t 20.0 -place top]
after 3000 [list scrollTextTo .t 80.0 -place bottom]
after 5000 [list scrollTextTo .t 30.0 -place center]