string geometry

Richard Suchenwirth 2013-11-07 - Everything is a string, and every string has its string length. But if we want to display a string in a text widget with tight fitting (i.e. just the right size, not too big, not too small), we need to specify the "geometry" of a string, i.e. its width and height.

If we disregard word wrapping, things are very easy:

set height [regexp -all \n $str\n]

regexp normally stops at the first match, and returns 1 then (or 0 if there were none). With -all, it returns the number of matches, so we can use it as a character counter.

I have appended an extra newline at the end, because many strings don't have (and in fact don't need) it. Perfectionists may first do string trimright before that. And here's how to calculate the (not wrapped) width of a string:

proc string_width str {
    set w 0
    foreach line [split $str \n] {
        set length [string length $line]
        if {$length > $w} {set w $length}
    return $w

Now for the less trivial, and hence more interesting case of word wrapping. The original author of the string doesn't have to bother for layout, we will.

If the string is N characters long, a simple approximation for height and width would be ceil(sqrt($N)). As characters are typically about double as high than wide, and we want an aspect ratio of about 4:3(in pixels) or 8:3 (in character units), here's a first shot that happens to match the initial requirements:

set sample "String geometry\n\nThis is a long string meant to display in a\
Tk text widget. Some lines are very long, and then again there are paragraph\
breaks.\n\nThe text widget shall wrap words, but be neither too big nor too\
small, and have a pleasing aspect ratio (say, in pixels, 4:3 or 16:9)."

set length [string length $sample]
set w0 [expr round(8./3.*ceil(sqrt($length)))]
set h0 [expr round(3./8.*ceil(sqrt($length)))+[regexp -all \n $sample]]
pack [text .t -wrap word -width $w0 -height $h0]
.t insert end $sample

Note that the number of explicit newlines is added to the theoretical height.

Should it not match the requirements, increase the height until the bbox of the last character in the string is no longer {}...


RFox 2013-11-07 21:21:56:

Why not get the both in one shot? The proc below returns a list of the width/height of a string without resorting to regexps an appending newlines to anything (at least I think it does ...untested but pretty simple change).

proc string_geometry str {
    set w 0
    set h 0
    foreach line [split $str \n] {
        incr h
        set length [string length $line]
        if {$length > $w} {set w $length}
    return [list $w $h]

RS My first shot was similar to yours, except for the regexp. I have been told long ago that a regexp without any quantifiers (?, *) is optimized to work very fast, so I picked that. And returning a list requires the caller to break it up again...

ARR 2013-11-08 Any ideas how to handle embedded images or windows in the text widget?

RS: Well, the method described above gives a first approximation - often good, but sometimes the text widget has a line too many, when experimenting with different fonts and sizes. If you add images or windows, they are counted as one character each, although they'll mostly be much bigger.

In the next step, we can examine the bounding box of the last character

 set bbox [.t bbox end-1c]

If we get a list of four integers, the full text is in sight, but we may want to reduce the height to get rid of empty lines. If we get an empty string, the last character is out of sight, so increase the height and width until the last character is in sight again.