More work on paged mode

This commit is contained in:
Kovid Goyal 2016-04-29 19:59:18 +05:30
parent d6624cdc33
commit 01d2572a9f
2 changed files with 101 additions and 78 deletions

View File

@ -8,10 +8,12 @@ from gettext import install
from read_book.globals import set_boss, set_current_spine_item, current_layout_mode, current_spine_item, set_layout_mode
from read_book.resources import finalize_resources, unserialize_html
from read_book.flow_mode import flow_to_scroll_fraction, flow_onwheel, flow_onkeydown, layout as flow_layout
from read_book.paged_mode import layout as paged_layout, scroll_to_fraction as paged_scroll_to_fraction
from read_book.paged_mode import layout as paged_layout, scroll_to_fraction as paged_scroll_to_fraction, onwheel as paged_onwheel, onkeydown as paged_onkeydown
from read_book.settings import apply_settings
from utils import debounce
FORCE_FLOW_MODE = False
class Boss:
def __init__(self):
@ -62,7 +64,7 @@ class Boss:
self.book = data.book
spine = self.book.manifest.spine
index = spine.indexOf(data.name)
set_layout_mode('flow' or data.settings.read_mode) # TODO: Change this
set_layout_mode('flow' if FORCE_FLOW_MODE else data.settings.read_mode)
apply_settings(data.settings)
set_current_spine_item({'name':data.name, 'is_first':index is 0, 'is_last':index is spine.length - 1, 'initial_scroll_fraction':data.initial_scroll_fraction})
root_data = finalize_resources(self.book, data.name, data.resource_data)
@ -95,10 +97,14 @@ class Boss:
evt.preventDefault()
if current_layout_mode() is 'flow':
flow_onwheel(evt)
else:
paged_onwheel(evt)
def onkeydown(self, evt):
if current_layout_mode() is 'flow':
flow_onkeydown(evt)
else:
paged_onkeydown(evt)
def send_message(self, action, **data):
data.action = action

View File

@ -49,12 +49,15 @@ def create_page_div(elem):
_in_paged_mode = False
def in_paged_mode():
return _in_paged_mode
col_width = page_width = screen_width = side_margin = page_height = margin_top = margin_bottom = cols_per_screen = 0
col_width = screen_width = screen_height = cols_per_screen = gap = col_and_gap = 0
is_full_screen_layout = False
def column_at(xpos):
# Return the number of the column that contains xpos
return xpos // page_width
# Return the (zero-based) number of the column that contains xpos
sw = document.body.scrollWidth
if xpos >= sw - col_and_gap:
xpos = sw - col_width + 10
return (xpos + gap) // col_and_gap
def fit_images():
# Ensure no images are wider than the available width in a column. Note
@ -62,7 +65,7 @@ def fit_images():
# force a relayout if the render tree is dirty.
images = []
vimages = []
maxh = page_height
maxh = screen_height
for img in document.getElementsByTagName('img'):
previously_limited = get_elem_data(img, 'width-limited', False)
data = get_elem_data(img, 'img-data', None)
@ -71,11 +74,10 @@ def fit_images():
data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display}
set_elem_data(img, 'img-data', data)
left = viewport_to_document(br.left, 0, img.ownerDocument)[0]
col = column_at(left) * page_width
rleft = left - col - side_margin
col = column_at(left) * col_and_gap
rleft = left - col
width = br.right - br.left
rright = rleft + width
col_width = page_width - 2*side_margin
if previously_limited or rright > col_width:
images.push([img, col_width - rleft])
previously_limited = get_elem_data(img, 'height-limited', False)
@ -101,7 +103,7 @@ def fit_images():
def layout(is_single_page):
nonlocal _in_paged_mode, col_width, page_width, page_height, side_margin, screen_width, margin_top, margin_bottom, is_full_screen_layout, cols_per_screen
nonlocal _in_paged_mode, col_width, col_and_gap, screen_height, gap, screen_width, is_full_screen_layout, cols_per_screen
body_style = window.getComputedStyle(document.body)
first_layout = not _in_paged_mode
if first_layout:
@ -116,35 +118,25 @@ def layout(is_single_page):
num -= 1
create_page_div()
ww = window.innerWidth
# Calculate the column width so that cols_per_screen columns fit in the
# window in such a way the right margin of the last column is <=
# side_margin (it may be less if the window width is not a
# multiple of n*(col_width+2*side_margin).
n = cols_per_screen = opts.cols_per_screen
adjust = ww - (ww // n) * n
# Ensure that the margins are large enough that the adjustment does not
# cause them to become negative semidefinite
sm = max(2*adjust, opts.margin_side)
# Minimum column width, for the cases when the window is too
# narrow
col_width = max(100, ((ww - adjust)/n) - 2*sm)
if opts.max_col_width > 0 and col_width > opts.max_col_width:
# Increase the side margin to ensure that col_width is no larger
# than opts.max_col_width
sm += Math.ceil( (col_width - opts.max_col_width) / 2*n )
col_width = max(100, ((ww - adjust)/n) - 2*sm)
page_width = col_width + 2*sm
side_margin = sm
screen_width = page_width * n
page_height = window.innerHeight
# Calculate the column width so that cols_per_screen columns fit exactly in
# the window width, with their separator margins
ww = col_width = screen_width = window.innerWidth
gap = 0
if n > 1:
# Adjust the side margin so that the window width satisfies
# col_width * n + (n-1) * 2 * side_margin = window_width
sm = opts.margin_side or 0
gap = 2*sm + ((ww + 2*sm) % n) # Ensure ww + gap is a multiple of n
col_width = ((ww + gap) // n) - gap
set_css(document.body, column_gap=2*sm + 'px', column_width=col_width + 'px', column_rule='0px inset blue',
screen_height = window.innerHeight
col_and_gap = col_width + gap
set_css(document.body, column_gap=gap + 'px', column_width=col_width + 'px', column_rule='0px inset blue',
min_width='0', max_width='none', min_height='0', max_height='none',
margin='0', border_width='0', padding='0 ' + sm + 'px',
width=(window.innerWidth - 2*sm) + 'px', height=window.innerHeight + 'px'
margin='0', border_width='0', padding='0',
width=screen_width + 'px', height=screen_height + 'px'
)
# Without this, webkit bleeds the margin of the first block(s) of body
# above the columns, which causes them to effectively be added to the
@ -176,12 +168,7 @@ def layout(is_single_page):
# with height=100% overflow the first column
has_svg = document.getElementsByTagName('svg').length > 0
only_img = document.getElementsByTagName('img').length is 1 and document.getElementsByTagName('div').length < 3 and document.getElementsByTagName('p').length < 2
# We only set full_screen_layout if scrollWidth is in (body_width,
# 2*body_width) as if it is <= body_width scrolling will work
# anyway and if it is >= 2*body_width it can't be a full screen
# layout
body_width = document.body.offsetWidth + 2 * sm
is_full_screen_layout = (only_img or has_svg) and single_screen and (document.body.scrollWidth <= body_width or (body_width < document.body.scrollWidth < 2 * body_width))
is_full_screen_layout = (only_img or has_svg) and single_screen and (document.body.scrollWidth < 2*ww + 10)
if is_single_page:
is_full_screen_layout = True
@ -191,21 +178,24 @@ def layout(is_single_page):
evt.preventDefault()
)
# Ensure the body width is an exact multiple of the column widths so that
# the browser does not adjust the column widths
# body_width = ncols * col_width + (ncols-1) * 2 * sm
ncols = document.body.scrollWidth / page_width
if ncols is not Math.floor(ncols) and not is_full_screen_layout:
set_css(document.body, width=Math.floor(ncols) * page_width - 2 * sm)
ncols = (document.body.scrollWidth + gap) / col_and_gap
if ncols is not Math.floor(ncols):
n = Math.floor(ncols)
dw = (n*col_width + (n-1)*gap)
document.body.width = dw + 'px'
print('WARNING: column layout broken', {'col_with':col_width, 'gap':gap, 'scrollWidth':document.body.scrollWidth, 'ncols':ncols, 'delta':document.body.scrollWidth - dw})
_in_paged_mode = True
fit_images()
return sm
return gap
def scroll_to_fraction(frac):
# Scroll to the position represented by frac (number between 0 and 1)
xpos = Math.floor(document.body.scrollWidth * frac)
scroll_to_xpos(xpos)
def scroll_to_column(number, animated=False, notify=False, duration=1000):
pos = number * col_and_gap
limit = document.body.scrollWidth - screen_width
pos = min(pos, limit)
if animated:
animated_scroll(pos, duration=1000, notify=notify)
else:
window.scrollTo(pos, 0)
def scroll_to_xpos(xpos, animated=False, notify=False, duration=1000):
# Scroll so that the column containing xpos is the left most column in
@ -216,16 +206,12 @@ def scroll_to_xpos(xpos, animated=False, notify=False, duration=1000):
if is_full_screen_layout:
window.scrollTo(0, 0)
return
pos = Math.floor(xpos/page_width) * page_width
limit = document.body.scrollWidth - screen_width
pos = min(pos, limit)
if animated:
animated_scroll(pos, duration=1000, notify=notify)
else:
window.scrollTo(pos, 0)
scroll_to_column(column_at(xpos), animated=animated, notify=notify, duration=duration)
def scroll_to_column(number):
scroll_to_xpos(number * page_width + 10)
def scroll_to_fraction(frac):
# Scroll to the position represented by frac (number between 0 and 1)
xpos = Math.floor(document.body.scrollWidth * frac)
scroll_to_xpos(xpos)
current_scroll_animation = None
@ -267,11 +253,11 @@ def column_location(elem):
else:
left, top = viewport_to_document(br.left, br.top, elem.ownerDocument)
c = column_at(left)
width = min(br.right, (c+1)*page_width) - br.left
width = min(br.right, (c+1)*col_and_gap) - br.left
if br.bottom < br.top:
br.bottom = window.innerHeight
height = min(br.bottom, window.innerHeight) - br.top
left -= c*page_width
left -= c * col_and_gap
return {'column':c, 'left':left, 'top':top, 'width':width, 'height':height}
def column_boundaries():
@ -292,8 +278,8 @@ def current_column_location():
# visible in the viewport
if is_full_screen_layout:
return 0
x = window.pageXOffset + max(10, side_margin)
return Math.floor(x/page_width) * page_width
c = column_at(window.pageXOffset + 10)
return c * col_and_gap
def next_screen_location():
# The position to scroll to for the next screen (which could contain
@ -303,9 +289,10 @@ def next_screen_location():
cc = current_column_location()
ans = cc + screen_width
if cols_per_screen > 1:
width_left = document.body.scrollWidth - (window.pageXOffset + window.innerWidth)
pages_left = width_left / page_width
if Math.ceil(pages_left) < cols_per_screen:
current_col = column_at(window.pageXOffset + 10)
ncols = (document.body.scrollWidth + gap) // col_and_gap
cols_left = ncols - (current_col + cols_per_screen)
if cols_left < cols_per_screen:
return -1 # Only blank, dummy pages left
limit = document.body.scrollWidth - window.innerWidth
if ans > limit:
@ -332,7 +319,7 @@ def next_col_location():
if is_full_screen_layout:
return -1
cc = current_column_location()
ans = cc + page_width
ans = cc + col_and_gap
limit = document.body.scrollWidth - window.innerWidth
if ans > limit:
ans = limit if window.pageXOffset < limit else -1
@ -345,7 +332,7 @@ def previous_col_location():
if is_full_screen_layout:
return -1
cc = current_column_location()
ans = cc - page_width
ans = cc - col_and_gap
if ans < 0:
ans = 0 if window.pageXOffset > 0 else -1
return ans
@ -390,7 +377,7 @@ def jump_to_anchor(name):
else:
left = br.left
scroll_to_xpos(viewport_to_document(
left+side_margin, elem.scrollTop, elem.ownerDocument)[0])
left+2, elem.scrollTop, elem.ownerDocument)[0])
def snap_to_selection():
# Ensure that the viewport is positioned at the start of the column
@ -430,18 +417,18 @@ def current_cfi():
# return ans
if in_paged_mode:
c = current_column_location()
for x in c, c-page_width, c+page_width:
for x in c, c - col_and_gap, c + col_and_gap:
# Try the current column, the previous column and the next
# column. Each column is tried from top to bottom.
left, right = x, x + page_width
left, right = x, x + col_and_gap
if left < 0 or right > document.body.scrollWidth:
continue
deltax = page_width // 25
deltax = col_and_gap // 25
deltay = window.innerHeight // 25
cury = 0
while cury < window.innerHeight:
curx = left + side_margin
while curx < right - side_margin:
curx = left
while curx < right - gap:
cfi = cfi_at_point(curx-window.pageXOffset, cury-window.pageYOffset)
if cfi:
# print('Viewport cfi:', cfi)
@ -465,3 +452,33 @@ def onwheel(evt):
get_boss().send_message('next_spine_item', previous=backward)
else:
scroll_to_xpos(x)
def onkeydown(evt):
handled = False
key = get_key(evt)
if key is 'up' or key is 'down':
handled = True
if evt.ctrlKey:
goto_boundary(-1 if key is 'up' else 1)
else:
smooth_y_scroll(key is 'up')
elif key is 'left' or key is 'right':
handled = True
if evt.ctrlKey:
window.scrollTo(0 if key is 'left' else document_width(), window.pageYOffset)
else:
window.scrollBy(-15 if key is 'left' else 15, 0)
elif key is 'home' or key is 'end':
handled = True
if evt.ctrlKey:
get_boss().send_message('goto_doc_boundary', start=key is 'home')
else:
if key is 'home':
window.scrollTo(0, 0)
else:
window.scrollTo(document.body.scrollWidth, 0)
elif key is 'pageup' or key is 'pagedown' or key is 'space':
handled = True
scroll_by_page(-1 if key is 'pageup' else 1)
if handled:
evt.preventDefault()