From 01d2572a9ff4586eef604ec52ee627d4e757eb1b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 29 Apr 2016 19:59:18 +0530 Subject: [PATCH] More work on paged mode --- src/pyj/read_book/iframe.pyj | 10 +- src/pyj/read_book/paged_mode.pyj | 169 +++++++++++++++++-------------- 2 files changed, 101 insertions(+), 78 deletions(-) diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index f767e25d51..4309afb603 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -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 diff --git a/src/pyj/read_book/paged_mode.pyj b/src/pyj/read_book/paged_mode.pyj index b4f08a2490..dc7d7b9b1d 100644 --- a/src/pyj/read_book/paged_mode.pyj +++ b/src/pyj/read_book/paged_mode.pyj @@ -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()