about summary refs log tree commit diff stats
Commit message (Expand)AuthorAgeFilesLines
...
* removed the i = textnw... as remarked by Martin HurtonAnselm R Garbe2008-08-181-1/+1
* applied Martin Hurton's scan() patch with slight modificationsAnselm R Garbe2008-08-181-4/+3
* changed order of variables in drawtextAnselm R Garbe2008-08-181-1/+1
* applied Martin Hurton's drawtext() patchAnselm R Garbe2008-08-181-3/+2
* applied Martin Hurton's view() simplification, not checking argAnselm R Garbe2008-08-181-2/+2
* making a comment more explicitAnselm R Garbe2008-08-181-1/+1
* removed setlocale() stuff, not necessary if Xmb in useAnselm R Garbe2008-08-181-2/+1
* added a comment about FAQ regarding mfact meaningAnselm R Garbe2008-08-181-1/+1
* added some comments regarding FAQ about s{x,y,w,h}, w{x,y,w,h}, b{y,h,lw}Anselm R Garbe2008-08-181-2/+4
* initialize trans with 0Anselm R Garbe2008-08-181-1/+1
* fix of XGetTransientForHint in manage()Premysl Hruby2008-08-141-3/+2
* applied cleanup from M. Hurton and S. NagyPremysl Hruby2008-08-141-2/+2
* fixes using arg->i instead of arg->uiPremysl Hruby2008-08-121-3/+3
* applied Johannes Hofmann's patch, please testanselm@anselm12008-08-041-40/+18
* made readin a config.h variableAnselm R Garbe2008-08-023-3/+3
* Added tag 5.1 for changeset ce355cea9bb8Anselm R Garbe2008-07-291-0/+1
* forcing fullscreen windows to bw=0, though most fullscreen apps are broken an... 5.1Anselm R Garbe2008-07-291-1/+1
* potential crash fix if xinerama behaves broken, though I doubt itAnselm R Garbe2008-07-181-2/+1
* make hg tip compilable with default configPremysl Hruby2008-07-181-2/+2
* local use of xidx is useless, got rid of it, falling back to screen 0 if poin...Anselm R Garbe2008-07-161-10/+7
* got rid of compile time xidx configuration, querying mouse pointer insteadAnselm R Garbe2008-07-162-20/+29
* reverted uint redefinitionAnselm R Garbe2008-07-161-46/+42
* applied anydot's urgency hint patch, thanks!anselm@aab2008-07-131-2/+25
* renamed eprint dieAnselm R Garbe2008-07-031-19/+19
* simplified detach()Anselm R Garbe2008-07-031-9/+3
* removed aux* stuff from ClientAnselm R Garbe2008-07-021-4/+0
* removed useless commentAnselm R Garbe2008-07-021-1/+1
* minor fix to view()Anselm R Garbe2008-07-021-0/+6
* applied Frederik Ternerot's grabbuttons patch with slight modificationsAnselm R Garbe2008-07-011-9/+8
* locale updateAnselm R Garbe2008-06-301-1/+3
* removed useless charactersAnselm R Garbe2008-06-241-3/+3
* applied James Turner's dwm.1 patch, thanks James!Anselm R Garbe2008-06-231-4/+1
* fix of monocleAnselm R Garbe2008-06-221-1/+1
* another mergeAnselm R Garbe2008-06-221-2/+2
|\
| * fixed Gottox' buttonpress/ClkTagBar codeAnselm R Garbe2008-06-211-2/+2
* | does this fix anything?Anselm R Garbe2008-06-221-1/+1
|/
* applied Gottox' ClkTagBar patchAnselm R Garbe2008-06-202-24/+15
* made arrange again like it was onceAnselm R Garbe2008-06-191-15/+15
* fixAnselm R Garbe2008-06-191-1/+1
* use sel instead of seeking the listAnselm R Garbe2008-06-191-7/+5
* resize should apply if !bannedAnselm R Garbe2008-06-191-3/+2
* applied Gottox patchAnselm R Garbe2008-06-191-25/+21
* untested monocleAnselm R Garbe2008-06-194-45/+55
* non-zeroAnselm R Garbe2008-06-192-3/+11
* Added tag 5.0 for changeset 06eb9644e2daAnselm R Garbe2008-06-181-0/+1
* branch merge 5.0Anselm R Garbe2008-06-171-1/+1
|\
| * s/tags ref/tags mask/arg@suckless.org2008-06-151-1/+1
* | tiled layout resizehints should be respected by defaultAnselm R Garbe2008-06-172-5/+5
* | restored y-coordinate fixing of client windowsAnselm R Garbe2008-06-171-1/+2
|/
* updated man page regarding Mod1-mAnselm R Garbe2008-06-151-1/+1
Generic.Traceback */ .highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #008800 } /* Keyword.Pseudo */ .highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
-- primitives for editing text
Text = {}

local utf8 = require 'utf8'

local Debug_new_render = false

-- return values:
--  y coordinate drawn until in px
--  position of start of final screen line drawn
function Text.draw(line, line_width, line_index)
  love.graphics.setColor(0,0,0)
  -- wrap long lines
  local x = 25
  local y = line.y
  local pos = 1
  local screen_line_starting_pos = 1
  if line.fragments == nil then
    Text.compute_fragments(line, line_width)
  end
  line.screen_line_starting_pos = nil  -- TODO: avoid recomputing on every repaint
  if Debug_new_render then print('--') end
  for _, f in ipairs(line.fragments) do
    local frag, frag_text = f.data, f.text
    -- render fragment
    local frag_width = math.floor(frag_text:getWidth()*Zoom)
    if x + frag_width > line_width then
      assert(x > 25)  -- no overfull lines
      if line_index > Screen_top1.line or pos > Screen_top1.pos then
        y = y + math.floor(15*Zoom)
        if New_foo then print('text: new screen line', y, Screen_height, screen_line_starting_pos) end
        screen_line_starting_pos = pos
        if Debug_new_render then print('y', y) end
      end
      x = 25
      if line.screen_line_starting_pos == nil then
        line.screen_line_starting_pos = {1, pos}
      else
        table.insert(line.screen_line_starting_pos, pos)
      end
      if line_index > Screen_top1.line or pos > Screen_top1.pos then
        if y + math.floor(15*Zoom) >= Screen_height then
          return y, screen_line_starting_pos
        end
      end
    end
    if Debug_new_render then print('checking to draw', pos, Screen_top1.pos) end
    if line_index > Screen_top1.line or pos >= Screen_top1.pos then
      if Debug_new_render then print('drawing '..frag) end
      love.graphics.draw(frag_text, x,y, 0, Zoom)
    end
    -- render cursor if necessary
    local frag_len = utf8.len(frag)
    if line_index == Cursor1.line then
      if pos <= Cursor1.pos and pos + frag_len > Cursor1.pos then
        Text.draw_cursor(x+Text.cursor_x2(frag, Cursor1.pos-pos+1), y)
      end
    end
    x = x + frag_width
    pos = pos + frag_len
  end
  if line_index == Cursor1.line and Cursor1.pos == pos then
    Text.draw_cursor(x, y)
  end
  Debug_new_render = false
  return y, screen_line_starting_pos
end
-- manual tests:
--  draw with small line_width of 100
--  short words break on spaces
--  long words break when they must

function Text.draw_cursor(x, y)
  love.graphics.setColor(1,0,0)
  love.graphics.circle('fill', x,y+math.floor(15*Zoom), 2)
  love.graphics.setColor(0,0,0)
  Cursor_x = x
  Cursor_y = y+math.floor(15*Zoom)
end

function Text.compute_fragments(line, line_width)
  line.fragments = {}
  local x = 25
  -- try to wrap at word boundaries
  for frag in line.data:gmatch('%S*%s*') do
    local frag_text = love.graphics.newText(love.graphics.getFont(), frag)
    local frag_width = math.floor(frag_text:getWidth()*Zoom)
--?     print('x: '..tostring(x)..'; '..tostring(line_width-x)..'px to go')
--?     print('frag: ^'..frag..'$ is '..tostring(frag_width)..'px wide')
    if x + frag_width > line_width then
      while x + frag_width > line_width do
        if x < 0.8*line_width then
          -- long word; chop it at some letter
          -- We're not going to reimplement TeX here.
          local b = Text.nearest_cursor_pos(frag, line_width - x)
--?           print('space for '..tostring(b)..' graphemes')
          local frag1 = string.sub(frag, 1, b)
          local frag1_text = love.graphics.newText(love.graphics.getFont(), frag1)
          local frag1_width = math.floor(frag1_text:getWidth()*Zoom)
--?           print('inserting '..frag1..' of width '..tostring(frag1_width)..'px')
          table.insert(line.fragments, {data=frag1, text=frag1_text})
          frag = string.sub(frag, b+1)
          frag_text = love.graphics.newText(love.graphics.getFont(), frag)
          frag_width = math.floor(frag_text:getWidth()*Zoom)
        end
        x = 25  -- new line
      end
    end
    if #frag > 0 then
--?       print('inserting '..frag..' of width '..tostring(frag_width)..'px')
      table.insert(line.fragments, {data=frag, text=frag_text})
    end
  end
end

function Text.textinput(t)
  if love.mouse.isDown('1') then return end
  Text.insert_at_cursor(t)
end

function Text.insert_at_cursor(t)
  local byte_offset
  if Cursor1.pos > 1 then
    byte_offset = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos-1)
  else
    byte_offset = 0
  end
  Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_offset)..t..string.sub(Lines[Cursor1.line].data, byte_offset+1)
  Lines[Cursor1.line].fragments = nil
  Cursor1.pos = Cursor1.pos+1
end

-- Don't handle any keys here that would trigger love.textinput above.
function Text.keychord_pressed(chord)
--?   Debug_new_render = true
  if chord == 'return' then
    local byte_offset = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
    table.insert(Lines, Cursor1.line+1, {mode='text', data=string.sub(Lines[Cursor1.line].data, byte_offset)})
    Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_offset-1)
    Lines[Cursor1.line].fragments = nil
    Cursor1.line = Cursor1.line+1
    Cursor1.pos = 1
    save_to_disk(Lines, Filename)
  elseif chord == 'tab' then
    Text.insert_at_cursor('\t')
    save_to_disk(Lines, Filename)
  elseif chord == 'left' then
    assert(Lines[Cursor1.line].mode == 'text')
    if Cursor1.pos > 1 then
      Cursor1.pos = Cursor1.pos-1
    else
      local new_cursor_line = Cursor1.line
      while new_cursor_line > 1 do
        new_cursor_line = new_cursor_line-1
        if Lines[new_cursor_line].mode == 'text' then
          Cursor1.line = new_cursor_line
          Cursor1.pos = utf8.len(Lines[Cursor1.line].data) + 1
          break
        end
      end
      if Cursor1.line < Screen_top1.line then
        Screen_top1.line = Cursor1.line
      end
    end
  elseif chord == 'right' then
    assert(Lines[Cursor1.line].mode == 'text')
    if Cursor1.pos <= utf8.len(Lines[Cursor1.line].data) then
      Cursor1.pos = Cursor1.pos+1
    else
      local new_cursor_line = Cursor1.line
      while new_cursor_line <= #Lines-1 do
        new_cursor_line = new_cursor_line+1
        if Lines[new_cursor_line].mode == 'text' then
          Cursor1.line = new_cursor_line
          Cursor1.pos = 1
          break
        end
      end
      if Cursor1.line > Screen_bottom1.line then
        Screen_top1.line = Cursor1.line
      end
    end
  elseif chord == 'home' then
    Cursor1.pos = 1
  elseif chord == 'end' then
    Cursor1.pos = utf8.len(Lines[Cursor1.line].data) + 1
  elseif chord == 'backspace' then
    if Cursor1.pos > 1 then
      local byte_start = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos-1)
      local byte_end = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
      if byte_start then
        if byte_end then
          Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_start-1)..string.sub(Lines[Cursor1.line].data, byte_end)
        else
          Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_start-1)
        end
        Lines[Cursor1.line].fragments = nil
        Cursor1.pos = Cursor1.pos-1
      end
    elseif Cursor1.line > 1 then
      if Lines[Cursor1.line-1].mode == 'drawing' then
        table.remove(Lines, Cursor1.line-1)
      else
        -- join lines
        Cursor1.pos = utf8.len(Lines[Cursor1.line-1].data)+1
        Lines[Cursor1.line-1].data = Lines[Cursor1.line-1].data..Lines[Cursor1.line].data
        Lines[Cursor1.line-1].fragments = nil
        table.remove(Lines, Cursor1.line)
      end
      Cursor1.line = Cursor1.line-1
    end
    save_to_disk(Lines, Filename)
  elseif chord == 'delete' then
    if Cursor1.pos <= utf8.len(Lines[Cursor1.line].data) then
      local byte_start = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos)
      local byte_end = utf8.offset(Lines[Cursor1.line].data, Cursor1.pos+1)
      if byte_start then
        if byte_end then
          Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_start-1)..string.sub(Lines[Cursor1.line].data, byte_end)
        else
          Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_start-1)
        end
        Lines[Cursor1.line].fragments = nil
        -- no change to Cursor1.pos
      end
    elseif Cursor1.line < #Lines then
      if Lines[Cursor1.line+1].mode == 'drawing' then
        table.remove(Lines, Cursor1.line+1)
      else
        -- join lines
        Lines[Cursor1.line].data = Lines[Cursor1.line].data..Lines[Cursor1.line+1].data
        Lines[Cursor1.line].fragments = nil
        table.remove(Lines, Cursor1.line+1)
      end
    end
    save_to_disk(Lines, Filename)
  elseif chord == 'up' then
    assert(Lines[Cursor1.line].mode == 'text')
--?     print('up', Cursor1.pos, Screen_top1.pos)
    local screen_line_index,screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line()
    if screen_line_starting_pos == 1 then
--?       print('cursor is at first screen line of its line')
      -- line is done; skip to previous text line
      local new_cursor_line = Cursor1.line
      while new_cursor_line > 1 do
        new_cursor_line = new_cursor_line-1
        if Lines[new_cursor_line].mode == 'text' then
          Cursor1.line = new_cursor_line
          if Lines[Cursor1.line].screen_line_starting_pos == nil then
            Cursor1.pos = Text.nearest_cursor_pos(Lines[Cursor1.line].data, Cursor_x)
            break
          end
          -- previous text line found, pick its final screen line
          local screen_line_starting_pos = Lines[Cursor1.line].screen_line_starting_pos
          screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]
--?           print('previous screen line starts at pos '..tostring(screen_line_starting_pos)..' of its line')
          if Screen_top1.line == Cursor1.line and Screen_top1.pos == screen_line_starting_pos then
            Screen_top1.pos = screen_line_starting_pos
--?             print('pos of top of screen is also '..tostring(Screen_top1.pos)..' of the same line')
          end
          local s = string.sub(Lines[Cursor1.line].data, screen_line_starting_pos)
          Cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, Cursor_x) - 1
          break
        end
      end
      if Cursor1.line < Screen_top1.line then
        Screen_top1.line = Cursor1.line
      end
    else
      -- move up one screen line in current line
--?       print('cursor is NOT at first screen line of its line')
      assert(screen_line_index > 1)
      new_screen_line_starting_pos = Lines[Cursor1.line].screen_line_starting_pos[screen_line_index-1]
--?       print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
      if Screen_top1.line == Cursor1.line and Screen_top1.pos == screen_line_starting_pos then
        Screen_top1.pos = new_screen_line_starting_pos
--?         print('also setting pos of top of screen to '..tostring(Screen_top1.pos))
      end
      local s = string.sub(Lines[Cursor1.line].data, new_screen_line_starting_pos)
      Cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, Cursor_x) - 1
--?       print('cursor pos is now '..tostring(Cursor1.pos))
    end
  elseif chord == 'down' then
    assert(Lines[Cursor1.line].mode == 'text')
    print('down', Cursor1.line, Cursor1.pos, Screen_top1.line, Screen_top1.pos, Screen_bottom1.line, Screen_bottom1.pos)
    if Text.cursor_at_final_screen_line() then
      -- line is done, skip to next text line
      print('cursor at final screen line of its line')
--?       os.exit(1)
      local new_cursor_line = Cursor1.line
      while new_cursor_line < #Lines do
        new_cursor_line = new_cursor_line+1
        if Lines[new_cursor_line].mode == 'text' then
          Cursor1.line = new_cursor_line
          Cursor1.pos = Text.nearest_cursor_pos(Lines[Cursor1.line].data, Cursor_x)
          print(Cursor1.pos)
          break
        end
      end
      if Cursor1.line > Screen_bottom1.line then
        print('screen top before:', Screen_top1.line, Screen_top1.pos)
        Screen_top1.line = Cursor1.line
        print('scroll up preserving cursor')
        Text.scroll_up_while_cursor_on_screen()
        print('screen top after:', Screen_top1.line, Screen_top1.pos)
      end
    else
      -- move down one screen line in current line
      print('cursor is NOT at final screen line of its line')
      local screen_line_index, screen_line_starting_pos = Text.pos_at_start_of_cursor_screen_line()
      new_screen_line_starting_pos = Lines[Cursor1.line].screen_line_starting_pos[screen_line_index+1]
      print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
      local s = string.sub(Lines[Cursor1.line].data, new_screen_line_starting_pos)
      Cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, Cursor_x) - 1
      print('cursor pos is now', Cursor1.line, Cursor1.pos)
      Screen_top1.line = Cursor1.line
      print('scroll up preserving cursor')
      Text.scroll_up_while_cursor_on_screen()
      print('screen top after:', Screen_top1.line, Screen_top1.pos)
    end
    print('=>', Cursor1.line, Cursor1.pos, Screen_top1.line, Screen_top1.pos, Screen_bottom1.line, Screen_bottom1.pos)
  end
end

function Text.pos_at_start_of_cursor_screen_line()
  if Lines[Cursor1.line].screen_line_starting_pos == nil then
    return 1,1
  end
  for i=#Lines[Cursor1.line].screen_line_starting_pos,1,-1 do
    local spos = Lines[Cursor1.line].screen_line_starting_pos[i]
    if spos <= Cursor1.pos then
      return i,spos
    end
  end
  assert(false)
end

function Text.cursor_at_final_screen_line()
  if Lines[Cursor1.line].screen_line_starting_pos == nil then
    return true
  end
  screen_lines = Lines[Cursor1.line].screen_line_starting_pos
  print(screen_lines[#screen_lines], Cursor1.pos)
  return screen_lines[#screen_lines] <= Cursor1.pos
end

function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary()
  while Cursor1.line <= #Lines do
    if Lines[Cursor1.line].mode == 'text' then
      break
    end
    Cursor1.line = Cursor1.line + 1
  end
  -- hack: insert a text line at bottom of file if necessary
  if Cursor1.line > #Lines then
    assert(Cursor1.line == #Lines+1)
    table.insert(Lines, {mode='text', data=''})
  end
  if Cursor1.line > Screen_bottom1.line then
    Screen_top1.line = Cursor1.line
    Text.scroll_up_while_cursor_on_screen()
  end
end

function Text.scroll_up_while_cursor_on_screen()
  local top2 = Text.to2(Cursor1)
--?   print('cursor pos '..tostring(Cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
  local y = Screen_height - math.floor(15*Zoom)
  -- duplicate some logic from love.draw
  while true do
--?     print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
    if top2.line == 1 and top2.screen_line == 1 then break end
    if top2.screen_line > 1 or Lines[top2.line-1].mode == 'text' then
      local h = math.floor(15*Zoom)
      if y - h < 15 then  -- top margin = 15
        break
      end
      y = y - h
    else
      assert(top2.line > 1)
      assert(Lines[top2.line-1].mode == 'drawing')
      -- We currently can't draw partial drawings, so either skip it entirely
      -- or not at all.
      local h = 20 + Drawing.pixels(Lines[top2.line-1].h)
      if y - h < 15 then
        break
      end
--?       print('skipping drawing of height', h)
      y = y - h
    end
    top2 = Text.previous_screen_line(top2)
  end
--?   print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
  Screen_top1 = Text.to1(top2)
--?   print('top1 finally:', Screen_top1.line, Screen_top1.pos)
end

function Text.in_line(line, x,y)
  if line.y == nil then return false end  -- outside current page
  if x < 16 then return false end
  if y < line.y then return false end
  if line.screen_line_starting_pos == nil then return y < line.y + math.floor(15*Zoom) end
  return y < line.y + #line.screen_line_starting_pos * math.floor(15*Zoom)
end

function Text.move_cursor(line_index, line, mx, my)
  Cursor1.line = line_index
  if line.screen_line_starting_pos == nil then
    Cursor1.pos = Text.nearest_cursor_pos(line.data, mx)
    return
  end
  assert(line.fragments)
  assert(my >= line.y)
  -- duplicate some logic from Text.draw
  local y = line.y
  for screen_line_index,screen_line_starting_pos in ipairs(line.screen_line_starting_pos) do
    local nexty = y + math.floor(15*Zoom)
    if my < nexty then
      -- On all wrapped screen lines but the final one, clicks past end of
      -- line position cursor on final character of screen line.
      -- (The final screen line positions past end of screen line as always.)
      if mx > Line_width and screen_line_index < #line.screen_line_starting_pos then
        Cursor1.pos = line.screen_line_starting_pos[screen_line_index+1]
        return
      end
      local s = string.sub(line.data, screen_line_starting_pos)
      Cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, mx) - 1
      return
    end
    y = nexty
  end
  assert(false)
end
-- manual test:
--  line: abc
--        def
--        gh
--  fragments: abc, def, gh
--  click inside e
--  line_starting_pos = 1 + 3 = 4
--  nearest_cursor_pos('defgh', mx) = 2
--  Cursor1.pos = 4 + 2 - 1 = 5
-- manual test:
--  click inside h
--  line_starting_pos = 1 + 3 + 3 = 7
--  nearest_cursor_pos('gh', mx) = 2
--  Cursor1.pos = 7 + 2 - 1 = 8

function Text.nearest_cursor_pos(line, x)
  if x == 0 then
    return 1
  end
  local len = utf8.len(line)
  local max_x = Text.cursor_x(line, len+1)
  if x > max_x then
    return len+1
  end
  local left, right = 1, len+1
--?   print('--')
  while true do
    local curr = math.floor((left+right)/2)
    local currxmin = Text.cursor_x(line, curr)
    local currxmax = Text.cursor_x(line, curr+1)
--?     print(x, left, right, curr, currxmin, currxmax)
    if currxmin <= x and x < currxmax then
      return curr
    end
    if left >= right-1 then
      return right
    end
    if currxmin > x then
      right = curr
    else
      left = curr
    end
  end
  assert(false)
end

function Text.cursor_x(line_data, cursor_pos)
  local line_before_cursor = line_data:sub(1, cursor_pos-1)
  local text_before_cursor = love.graphics.newText(love.graphics.getFont(), line_before_cursor)
  return 25 + math.floor(text_before_cursor:getWidth()*Zoom)
end

function Text.cursor_x2(s, cursor_pos)
  local s_before_cursor = s:sub(1, cursor_pos-1)
  local text_before_cursor = love.graphics.newText(love.graphics.getFont(), s_before_cursor)
  return math.floor(text_before_cursor:getWidth()*Zoom)
end

function Text.to2(pos1)
  assert(Lines[pos1.line].mode == 'text')
  local result = {line=pos1.line, screen_line=1}
  if Lines[pos1.line].screen_line_starting_pos == nil then
    result.screen_pos = pos1.pos
  else
    for i=#Lines[pos1.line].screen_line_starting_pos,1,-1 do
      local spos = Lines[pos1.line].screen_line_starting_pos[i]
      if spos <= pos1.pos then
        result.screen_line = i
        result.screen_pos = pos1.pos - spos + 1
        break
      end
    end
  end
  assert(result.screen_pos)
  return result
end

function Text.to1(pos2)
  local result = {line=pos2.line, pos=pos2.screen_pos}
  if pos2.screen_line > 1 then
    result.pos = Lines[pos2.line].screen_line_starting_pos[pos2.screen_line] + pos2.screen_pos - 1
  end
  return result
end

function Text.previous_screen_line(pos2)
  if pos2.screen_line > 1 then
    return {line=pos2.line, screen_line=pos2.screen_line-1, screen_pos=1}
  elseif pos2.line == 1 then
    return pos2
  elseif Lines[pos2.line-1].mode == 'drawing' then
    return {line=pos2.line-1, screen_line=1, screen_pos=1}
  else
    local l = Lines[pos2.line-1]
    if l.screen_line_starting_pos == nil then
      return {line=pos2.line-1, screen_line=1, screen_pos=1}
    else
      return {line=pos2.line-1, screen_line=#Lines[pos2.line-1].screen_line_starting_pos, screen_pos=1}
    end
  end
end

return Text