Template repo for tiny cross-platform apps that can be modified on phone, tablet or computer.
at main 214 lines 7.2 kB view raw
1-- Line wrapping 2local my_utf8 = require 'my_utf8' 3 4local wrap = {} 5local I = {} 6wrap.internal = I 7 8-- include width for each codepoint 9function I.widths(s, startpos, font) 10 local next_char = my_utf8.chars(s, startpos) 11 return function() 12 local p, cp, char = next_char() 13 if not p then return nil end 14 return p, cp, char, font:getWidth(char) 15 end 16end 17 18local WHITESPACE = { 19 [0x0009] = true, [0x000A] = true, [0x000B] = true, [0x000C] = true, 20 [0x000D] = true, [0x0020] = true, [0x0085] = true, [0x00A0] = true, 21 [0x1680] = true, [0x2000] = true, [0x2001] = true, [0x2002] = true, 22 [0x2003] = true, [0x2004] = true, [0x2005] = true, [0x2006] = true, 23 [0x2007] = true, [0x2008] = true, [0x2009] = true, [0x200A] = true, 24 [0x2028] = true, [0x2029] = true, [0x202F] = true, [0x205F] = true, 25 [0x3000] = true, 26} 27 28local WORD_START = 1 29local WORD_END = 2 30 31-- tag codepoints at WORD_START (after space) and WORD_END (before space) 32function I.words(s, startpos, font) 33 local next_char = I.widths(s, startpos, font) 34 local was_space = true 35 return function() 36 local p, cp, char, w = next_char() 37 if not p then return nil end 38 local space = WHITESPACE[cp] 39 local state = nil 40 if not space and was_space then 41 state = WORD_START 42 elseif space and not was_space then 43 state = WORD_END 44 end 45 was_space = space 46 return p, cp, char, w, state 47 end 48end 49 50-- tag each WORD_START codepoint with width of word (WORD_START to WORD_END) 51function I.wordwidth(s, startpos, font) 52 local next_char = I.words(s, startpos, font) 53 return coroutine.wrap(function() 54 local pos, cp, char, w, state 55 while true do 56 pos, cp, char, w, state = next_char() 57 if not pos then return end 58 if state ~= WORD_START then 59 coroutine.yield(pos, cp, char, w, state, nil) 60 else 61 break 62 end 63 end 64 while pos do 65 assert(state == WORD_START) 66 -- buffer to next WORD_START 67 local buf = {{pos, cp, char, w, state}} 68 local word_w = w 69 while true do 70 pos, cp, char, w, state = next_char() 71 if not pos then break end 72 table.insert(buf, {pos, cp, char, w, state}) 73 if state == WORD_END then break end 74 word_w = word_w + w 75 end 76 -- emit buffer 77 table.insert(buf[1], word_w) -- just to the first char of word 78 for _,e in ipairs(buf) do 79 coroutine.yield(unpack(e)) 80 end 81 -- emit spaces to next word 82 if not pos then break end 83 assert(state == WORD_END) 84 while true do 85 pos, cp, char, w, state = next_char() 86 if not pos then break end 87 if state == WORD_START then break end 88 coroutine.yield(pos, cp, char, w, state) 89 end 90 end 91 end) 92end 93 94-- tag each WORD_START codepoint with total width of word+spaces (WORD_START to WORD_START) 95function I.totalwordwidth(s, startpos, font) 96 local next_char = I.wordwidth(s, startpos, font) 97 return coroutine.wrap(function() 98 local pos, cp, char, w, state, word_w 99 while true do 100 pos, cp, char, w, state, word_w = next_char() 101 if not pos then return end 102 if state ~= WORD_START then 103 coroutine.yield(pos, cp, char, w, state, word_w, nil) 104 else 105 break 106 end 107 end 108 while pos do 109 assert(state == WORD_START) 110 -- buffer to next WORD_START 111 local buf = {{pos, cp, char, w, state, word_w}} 112 local total_w = w 113 while true do 114 pos, cp, char, w, state, word_w = next_char() 115 if state == WORD_START or not pos then 116 break 117 end 118 table.insert(buf, {pos, cp, char, w, state, word_w}) 119 total_w = total_w + w 120 end 121 -- emit buffer 122 table.insert(buf[1], total_w) -- just to the first char of word 123 for _,e in ipairs(buf) do 124 coroutine.yield(unpack(e)) 125 end 126 end 127 end) 128end 129 130function wrap.wrap(s, startpos, min_width, max_width, font) 131 local next_char = I.totalwordwidth(s, startpos, font) 132 local x = 0 133 local limit = max_width 134 return function() 135 while true do 136 local p, cp, char, w, state, word_w, total_w = next_char() 137 local is_wrap 138 if not p then return nil end 139 if state == WORD_START then 140 limit = max_width 141 if x + total_w > max_width and x >= min_width then 142 -- Wrap at word boundary 143 x = 0 144 is_wrap = {word_wrap=true} 145 elseif x + word_w >= min_width and x + word_w < max_width and x + total_w > max_width then 146 -- We can't word wrap just yet, and the word also wouldn't fit 147 -- without pushing trailing spaces to the next line. 148 -- Plan to truncate word at min_width so the next line starts with 149 -- non-spaces. 150 limit = min_width 151 end 152 end 153 if x > 0 and x + w > limit then 154 -- overflow 155 x = 0 156 is_wrap = {} 157 assert(char ~= ' ') 158 limit = max_width 159 end 160 local char_x = x 161 x = x + w 162 return p, char, w, char_x, is_wrap 163 end 164 end 165end 166 167-- like wrap, but can indent the next line after wrapping 168function wrap.indented_wrap(s, startpos, min_width, max_width, font, wrap_indent, debug) 169 local next_char = I.totalwordwidth(s, startpos, font) 170 local x = 0 -- first line starts at 0 171 local limit = max_width 172 return function() 173 while true do 174 local p, cp, char, w, state, word_w, total_w = next_char() 175 local is_wrap 176 if not p then return nil end 177 if state == WORD_START then 178 limit = max_width 179 if x + total_w > max_width and x >= min_width then 180 -- Wrap at word boundary 181 if debug then 182 print_to_output(('word wrap instead of printing to %d because next word including trailing whitespace would take us to %d which is greater than %d. Line is longer than %d.'):format(x, x+total_w, max_width, min_width)) 183 end 184 is_wrap = {word_wrap=true, unwrapped_x=x} 185 x = wrap_indent 186 elseif x + word_w >= min_width and x + word_w < max_width and x + total_w > max_width then 187 -- We can't word wrap just yet, and the word also wouldn't fit 188 -- without pushing trailing spaces to the next line. 189 -- Plan to truncate word at min_width so the next line starts with 190 -- non-spaces. 191 if debug then 192 print_to_output(('Line length %d < min width %d and next word including trailing whitespace would take us to %d > max width %d. But just the next word would take us to %d < %d. So word wrap would put whitespace at the start of the next wrapped line. Instead truncate at %d.'):format(x, min_width, x + total_w, max_width, x + word_w, max_width, min_width)) 193 end 194 limit = min_width 195 end 196 end 197 if x > 0 and x + w > limit then 198 -- overflow 199 if debug then 200 print_to_output(('wrap at %d because next character would put us at %d > limit %d'):format(x, x+w, limit)) 201 end 202 is_wrap = {unwrapped_x=x} 203 x = wrap_indent 204 assert(char ~= ' ') 205 limit = max_width 206 end 207 local char_x = x 208 x = x + w 209 return p, char, w, char_x, is_wrap 210 end 211 end 212end 213 214return wrap