# vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal # Notes of paged mode scrolling: # All the math in paged mode is based on the block and inline directions. # Inline is "the direction lines of text go." # In horizontal scripts such as English and Hebrew, the inline is horizontal # and the block is vertical. # In vertical languages such as Japanese and Mongolian, the inline is vertical # and block is horizontal. # Regardless of language, paged mode scrolls by column in the inline direction, # because by the CSS spec, columns are laid out in the inline direction. # # In horizontal RTL books, such as Hebrew, the inline direction goes right to left. # |<------| # This means that the column positions become negative as you scroll. # This is hidden from paged mode by the viewport, which transparently # negates any inline coordinates sent in to the viewport_to_document* functions # and the various scroll_to/scroll_by functions, as well as the reported X position. # # The result of all this is that paged mode's math can safely pretend that # things scroll in the positive inline direction. from __python__ import hash_literals, bound_methods import traceback from elementmaker import E from dom import set_css from read_book.cfi import ( at_current as cfi_at_current, at_point as cfi_at_point, scroll_to as cfi_scroll_to ) from read_book.globals import current_spine_item, get_boss, rtl_page_progression from read_book.settings import opts from read_book.viewport import scroll_viewport, line_height, rem_size from utils import ( get_elem_data, set_elem_data ) def first_child(parent): c = parent.firstChild count = 0 while c?.nodeType is not Node.ELEMENT_NODE and count < 20: c = c?.nextSibling count += 1 if c?.nodeType is Node.ELEMENT_NODE: return c def has_start_text(elem): # Returns true if elem has some non-whitespace text before its first child # element for c in elem.childNodes: if c.nodeType is not Node.TEXT_NODE: break if c.nodeType is Node.TEXT_NODE and c.nodeValue and /\S/.test(c.nodeValue): return True return False def handle_rtl_body(body_style): if body_style.direction is "rtl": # If this in not set, Chrome scrolling breaks for some RTL and vertical content. document.documentElement.style.overflow = 'visible' def create_page_div(elem): div = E('blank-page-div', ' \n ') document.body.appendChild(div) set_css(div, break_before='always', display='block', white_space='pre', background_color='transparent', background_image='none', border_width='0', float='none', position='static') _in_paged_mode = False def in_paged_mode(): return _in_paged_mode col_size = screen_inline = screen_block = cols_per_screen = gap = col_and_gap = number_of_cols = last_scrolled_to_column = 0 is_full_screen_layout = False def reset_paged_mode_globals(): nonlocal _in_paged_mode, col_size, col_and_gap, screen_block, gap, screen_inline, is_full_screen_layout, cols_per_screen, number_of_cols, last_scrolled_to_column scroll_viewport.reset_globals() col_size = screen_inline = screen_block = cols_per_screen = gap = col_and_gap = number_of_cols = last_scrolled_to_column = 0 is_full_screen_layout = _in_paged_mode = False resize_manager.reset() def column_at(pos): # Return the (zero-based) number of the column that contains pos si = scroll_viewport.paged_content_inline_size() if pos >= si - col_and_gap: pos = si - col_size + 10 # we subtract 1 here so that a point at the absolute trailing (right in # horz-LTR) edge of a column remains in the column and not at the next column return max(0, (pos + gap - 1)) // col_and_gap def fit_images(): # Ensure no images are wider than the available size of a column. Note # that this method use getBoundingClientRect() which means it will # force a relayout if the render tree is dirty. inline_limited_images = v'[]' block_limited_images = v'[]' img_tags = document.getElementsByTagName('img') bounding_rects = v'[]' for img_tag in img_tags: bounding_rects.push(img_tag.getBoundingClientRect()) maxb = screen_block for i in range(img_tags.length): img = img_tags[i] br = bounding_rects[i] previously_limited = get_elem_data(img, 'inline-limited', False) data = get_elem_data(img, 'img-data', None) if data is None: data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display} set_elem_data(img, 'img-data', data) # Get start of image bounding box in the column direction (inline) image_start = scroll_viewport.viewport_to_document_inline(scroll_viewport.rect_inline_start(br), img.ownerDocument) col_start = column_at(image_start) * col_and_gap # Get inline distance from the start of the column to the start of the image bounding box column_start_to_image_start = image_start - col_start image_block_size = scroll_viewport.rect_block_size(br) image_inline_size = scroll_viewport.rect_inline_size(br) # Get the inline distance from the start of the column to the end of the image image_inline_end = column_start_to_image_start + image_inline_size # If the end of the image goes past the column, add it to the list of inline_limited_images if previously_limited or image_inline_end > col_size: inline_limited_images.push(v'[img, col_size - column_start_to_image_start]') previously_limited = get_elem_data(img, 'block-limited', False) if previously_limited or image_block_size > maxb or (image_block_size is maxb and image_inline_size > col_size): block_limited_images.push(img) if previously_limited: set_css(img, break_before='auto', display=data.display) set_css(img, break_inside='avoid') for img_tag, max_inline_size in inline_limited_images: if scroll_viewport.vertical_writing_mode: img_tag.style.setProperty('max-height', max_inline_size+'px') else: img_tag.style.setProperty('max-width', max_inline_size+'px') set_elem_data(img_tag, 'inline-limited', True) for img_tag in block_limited_images: if scroll_viewport.vertical_writing_mode: set_css(img_tag, break_before='always', max_width='100vw') else: set_css(img_tag, break_before='always', max_height='100vh') set_elem_data(img_tag, 'block-limited', True) def cps_by_em_size(): ans = cps_by_em_size.ans fs = window.getComputedStyle(document.body).fontSize if not ans or cps_by_em_size.at_font_size is not fs: d = document.createElement('span') d.style.position = 'absolute' d.style.visibility = 'hidden' d.style.width = '1rem' d.style.fontSize = '1rem' d.style.paddingTop = d.style.paddingBottom = d.style.paddingLeft = d.style.paddingRight = '0' d.style.marginTop = d.style.marginBottom = d.style.marginLeft = d.style.marginRight = '0' d.style.borderStyle = 'none' document.body.appendChild(d) w = d.clientWidth document.body.removeChild(d) ans = cps_by_em_size.ans = max(2, w) cps_by_em_size.at_font_size = fs return ans def calc_columns_per_screen(): cps = opts.columns_per_screen or {} cps = cps.landscape if scroll_viewport.width() > scroll_viewport.height() else cps.portrait try: cps = int(cps) except: cps = 0 if not cps: cps = int(Math.floor(scroll_viewport.inline_size() / (35 * cps_by_em_size()))) cps = max(1, min(cps or 1, 20)) return cps def get_columns_per_screen_data(): which = 'landscape' if scroll_viewport.width() > scroll_viewport.height() else 'portrait' return {'which': which, 'cps': calc_columns_per_screen()} def will_columns_per_screen_change(): return calc_columns_per_screen() != cols_per_screen class ScrollResizeBugWatcher: # In Chrome sometimes after layout the scrollWidth of body increases after a # few milliseconds, this can cause scrolling to the end to not work # immediately after layout. This happens without a resize event, and # without triggering the ResizeObserver and only in paged mode. def __init__(self): self.max_time = 750 self.last_layout_at = 0 self.last_command = None self.doc_size = 0 self.timer = None def layout_done(self): self.last_layout_at = window.performance.now() self.last_command = None self.cancel_timer() def scrolled(self, pos, limit): self.cancel_timer() now = window.performance.now() if now - self.last_layout_at < self.max_time and self.last_command is not None: self.doc_size = scroll_viewport.paged_content_inline_size() self.check_for_resize_bug() def cancel_timer(self): if self.timer is not None: window.clearTimeout(self.timer) self.timer = None def check_for_resize_bug(self): sz = scroll_viewport.paged_content_inline_size() if sz != self.doc_size: return self.redo_scroll() now = window.performance.now() if now - self.last_layout_at < self.max_time: window.setTimeout(self.check_for_resize_bug, 10) else: self.timer = None def redo_scroll(self): if self.last_command: self.last_command() self.last_command = None self.timer = None self.doc_size = 0 scroll_resize_bug_watcher = ScrollResizeBugWatcher() def layout(is_single_page, on_resize): nonlocal _in_paged_mode, col_size, col_and_gap, screen_block, gap, screen_inline, is_full_screen_layout, cols_per_screen, number_of_cols line_height(True) rem_size(True) body_style = window.getComputedStyle(document.body) scroll_viewport.initialize_on_layout(body_style) first_layout = not _in_paged_mode cps = calc_columns_per_screen() if first_layout: handle_rtl_body(body_style) # Check if the current document is a full screen layout like # cover, if so we treat it specially. single_screen = scroll_viewport.document_block_size() < (scroll_viewport.block_size() + 75) first_layout = True svgs = document.getElementsByTagName('svg') has_svg = svgs.length > 0 imgs = document.getElementsByTagName('img') only_img = imgs.length is 1 and document.getElementsByTagName('div').length < 3 and document.getElementsByTagName('p').length < 2 if only_img and window.getComputedStyle(imgs[0]).zIndex < 0: # Needed for some stupidly coded fixed layout EPUB comics, see for # instance: https://bugs.launchpad.net/calibre/+bug/1667357 imgs[0].style.zIndex = '0' if not single_screen and cps > 1: num = cps - 1 elems = document.querySelectorAll('body > *') if elems.length is 1: # Workaround for the case when the content is wrapped in a # 100% height
. This causes the generated page divs to # not be in the correct location, at least in WebKit. See # https://bugs.launchpad.net/bugs/1594657 for an example. elems[0].style.height = 'auto' while num > 0: num -= 1 create_page_div() n = cols_per_screen = cps # Calculate the column size so that cols_per_screen columns fit exactly in # the window inline dimension, with their separator margins wi = col_size = screen_inline = scroll_viewport.inline_size() margin_size = (opts.margin_left + opts.margin_right) if scroll_viewport.horizontal_writing_mode else (opts.margin_top + opts.margin_bottom) # a zero margin causes scrolling issues, see https://bugs.launchpad.net/calibre/+bug/1918437 margin_size = max(1, margin_size) gap = margin_size if n > 1: # Adjust the margin so that the window inline dimension satisfies # col_size * n + (n-1) * 2 * margin = window_inline gap += ((wi + margin_size) % n) # Ensure wi + gap is a multiple of n col_size = ((wi + gap) // n) - gap screen_block = scroll_viewport.block_size() col_and_gap = col_size + gap set_css(document.body, column_gap=gap + 'px', column_width=col_size + 'px', column_rule='0px inset blue', min_width='0', max_width='none', min_height='0', max_height='100vh', column_fill='auto', margin='0', border_width='0', padding='0', box_sizing='content-box', width=scroll_viewport.width() + 'px', height=scroll_viewport.height() + 'px', overflow_wrap='break-word' ) # 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 # page margins (the margin collapse algorithm) document.body.style.setProperty('-webkit-margin-collapse', 'separate') c = first_child(document.body) if c: # Remove page breaks on the first few elements to prevent blank pages # at the start of a chapter set_css(c, break_before='avoid') if c.tagName.toLowerCase() is 'div': c2 = first_child(c) if c2 and not has_start_text(c): # Common pattern of all content being enclosed in a wrapper #
, see for example: https://bugs.launchpad.net/bugs/1366074 # In this case, we also modify the first child of the div # as long as there was no text before it. set_css(c2, break_before='avoid') if first_layout: # Because of a bug in webkit column mode, svg elements defined with # width 100% are wider than body and lead to a blank page after the # current page (when cols_per_screen == 1). Similarly img elements # with height=100% overflow the first column is_full_screen_layout = is_single_page if not is_full_screen_layout: has_no_more_than_two_columns = (scroll_viewport.paged_content_inline_size() < 2*wi + 10) if has_no_more_than_two_columns and single_screen: if only_img and imgs.length and imgs[0].getBoundingClientRect().left < wi: is_full_screen_layout = True if has_svg and svgs.length == 1 and svgs[0].getBoundingClientRect().left < wi: is_full_screen_layout = True if is_full_screen_layout and only_img and cols_per_screen > 1: cols_per_screen = 1 col_size = screen_inline col_and_gap = col_size + gap number_of_cols = 1 document.body.style.columnWidth = f'100vw' # Some browser engine, WebKit at least, adjust column sizes to please # themselves, unless the container size is an exact multiple, so we check # for that and manually set the container sizes. def check_column_sizes(): nonlocal number_of_cols ncols = number_of_cols = (scroll_viewport.paged_content_inline_size() + gap) / col_and_gap if ncols is not Math.floor(ncols): data = {'col_size':col_size, 'gap':gap, 'scrollWidth':scroll_viewport.paged_content_inline_size(), 'ncols':ncols, 'desired_inline_size':dis} return data data = check_column_sizes() if data: dis = data.desired_inline_size for elem in document.documentElement, document.body: set_css(elem, max_width=dis + 'px', min_width=dis + 'px') if scroll_viewport.vertical_writing_mode: set_css(elem, max_height=dis + 'px', min_height=dis + 'px') data = check_column_sizes() if data: print('WARNING: column layout broken, probably because there is some non-reflowable content in the book whose inline size is greater than the column size', data) _in_paged_mode = True fit_images() scroll_resize_bug_watcher.layout_done() return gap def current_scroll_offset(): return scroll_viewport.inline_pos() def scroll_to_offset(offset): scroll_viewport.scroll_to_in_inline_direction(offset) def scroll_to_column(number, notify=False, duration=1000): nonlocal last_scrolled_to_column last_scrolled_to_column = number pos = number * col_and_gap limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size() pos = min(pos, limit) scroll_to_offset(pos) scroll_resize_bug_watcher.scrolled(pos, limit) def scroll_to_pos(pos, notify=False, duration=1000): nonlocal last_scrolled_to_column # Scroll to the column containing pos if jstype(pos) is not 'number': print(pos, 'is not a number, cannot scroll to it!') return if is_full_screen_layout: scroll_to_offset(0) last_scrolled_to_column = 0 return scroll_to_column(column_at(pos), notify=notify, duration=duration) def scroll_to_previous_position(fsd): fsd = fsd or next_spine_item.forward_scroll_data next_spine_item.forward_scroll_data = None if 0 < fsd.cols_left < cols_per_screen and cols_per_screen < number_of_cols: scroll_resize_bug_watcher.last_command = scroll_to_previous_position.bind(None, fsd) scroll_to_column(fsd.current_col) return True def scroll_to_fraction(frac, on_initial_load): # Scroll to the position represented by frac (number between 0 and 1) if on_initial_load and frac is 1 and is_return() and scroll_to_previous_position(): return scroll_resize_bug_watcher.last_command = scroll_to_fraction.bind(None, frac, False) pos = Math.floor(scroll_viewport.paged_content_inline_size() * frac) scroll_to_pos(pos) def column_boundaries(): # Return the column numbers at the left edge and after the right edge # of the viewport l = column_at(current_scroll_offset() + 10) return l, l + cols_per_screen def column_at_current_scroll_offset(): return column_at(current_scroll_offset() + 10) def current_column_location(): # The location of the starting edge of the first column currently # visible in the viewport if is_full_screen_layout: return 0 return column_at_current_scroll_offset() * col_and_gap def number_of_cols_left(): current_col = column_at(current_scroll_offset() + 10) cols_left = number_of_cols - (current_col + cols_per_screen) return Math.max(0, cols_left) def next_screen_location(): # The position to scroll to for the next screen (which could contain # more than one pages). Returns -1 if no further scrolling is possible. if is_full_screen_layout: return -1 cc = current_column_location() ans = cc + screen_inline + 1 if cols_per_screen > 1 and 0 < number_of_cols_left() < cols_per_screen: return -1 # Only blank, dummy pages left limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size() if limit < col_and_gap: return -1 if ans > limit: ans = limit if Math.ceil(current_scroll_offset()) < limit else -1 return ans def previous_screen_location(): # The position to scroll to for the previous screen (which could contain # more than one pages). Returns -1 if no further scrolling is possible. if is_full_screen_layout: return -1 cc = current_column_location() ans = cc - cols_per_screen * col_and_gap if ans < 0: # We ignore small scrolls (less than 15px) when going to previous # screen ans = 0 if current_scroll_offset() > 15 else -1 return ans def next_col_location(): # The position to scroll to for the next column (same as # next_screen_location() if columns per screen == 1). Returns -1 if no # further scrolling is possible. if is_full_screen_layout: return -1 cc = current_column_location() ans = cc + col_and_gap limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size() # print(f'cc={cc} col_and_gap={col_and_gap} ans={ans} limit={limit} content_inline_size={scroll_viewport.paged_content_inline_size()} inline={scroll_viewport.inline_size()} current_scroll_offset={current_scroll_offset()}') if ans > limit: if Math.ceil(current_scroll_offset()) < limit and column_at(limit) > column_at_current_scroll_offset(): ans = limit else: ans = -1 return ans def previous_col_location(): # The position to scroll to for the previous column (same as # previous_screen_location() if columns per screen == 1). Returns -1 if # no further scrolling is possible. if is_full_screen_layout: return -1 cc = current_column_location() ans = cc - col_and_gap if ans < 0: if Math.floor(current_scroll_offset()) > 0 and column_at(0) < column_at_current_scroll_offset(): ans = 0 else: ans = -1 return ans def jump_to_anchor(name): # Jump to the element identified by anchor name. elem = document.getElementById(name) if not elem: elems = document.getElementsByName(name) if elems: elem = elems[0] if not elem: return scroll_to_elem(elem) def scrollable_element(elem): # bounding rect calculation for an inline element containing a block # element that spans multiple columns is incorrect. Detet the common case # of this and avoid it. See https://bugs.launchpad.net/calibre/+bug/1918437 # for a test case. if not in_paged_mode() or window.getComputedStyle(elem).display.indexOf('inline') < 0 or not elem.firstElementChild: return elem if window.getComputedStyle(elem.firstElementChild).display.indexOf('block') > -1 and elem.getBoundingClientRect().top < -100: return elem.firstElementChild return elem def scroll_to_elem(elem): elem = scrollable_element(elem) scroll_viewport.scroll_into_view(elem) if in_paged_mode(): # Ensure we are scrolled to the column containing elem # Because of a bug in WebKit's getBoundingClientRect() in column # mode, this position can be inaccurate, see # https://bugs.launchpad.net/calibre/+bug/1132641 for a test case. # The usual symptom of the inaccuracy is br.top is highly negative. br = elem.getBoundingClientRect() if br.top < -100: # This only works because of the preceding call to # elem.scrollIntoView(). However, in some cases it gives # inaccurate results, so we prefer the bounding client rect, # when possible. # In horizontal writing, the inline start position depends on the direction if scroll_viewport.horizontal_writing_mode: inline_start = elem.scrollLeft if scroll_viewport.ltr else elem.scrollRight # In vertical writing, the inline start position is always the top since # vertical text only flows top-to-bottom else: inline_start = elem.scrollTop else: # If we can use the rect, just use the simpler viewport helper function inline_start = scroll_viewport.rect_inline_start(br) scroll_to_pos(scroll_viewport.viewport_to_document_inline(inline_start+2, elem.ownerDocument)) def snap_to_selection(): # Ensure that the viewport is positioned at the start of the column # containing the start of the current selection if in_paged_mode(): sel = window.getSelection() r = sel.getRangeAt(0).getBoundingClientRect() node = sel.anchorNode # Columns are in the inline direction, so get the beginning of the element in the inline pos = scroll_viewport.viewport_to_document_inline( scroll_viewport.rect_inline_start(r), doc=node.ownerDocument) # Ensure we are scrolled to the column containing the start of the # selection scroll_to_pos(pos+5) def ensure_selection_boundary_visible(use_end): sel = window.getSelection() try: rr = sel.getRangeAt(0) except: rr = None if rr: r = rr.getBoundingClientRect() if r: cnum = column_at_current_scroll_offset() scroll_to_column(cnum) node = sel.focusNode if use_end else sel.anchorNode # Columns are in the inline direction, so get the beginning of the element in the inline x = scroll_viewport.rect_inline_end(r) if use_end else scroll_viewport.rect_inline_start(r) if x < 0 or x >= scroll_viewport.inline_size(): pos = scroll_viewport.viewport_to_document_inline(x, doc=node.ownerDocument) scroll_to_pos(pos+5) def jump_to_cfi(cfi): # Jump to the position indicated by the specified conformal fragment # indicator. scroll_resize_bug_watcher.last_command = jump_to_cfi.bind(None, cfi) cfi_scroll_to(cfi, def(x, y): if scroll_viewport.horizontal_writing_mode: scroll_to_pos(x) else: scroll_to_pos(y) ) def current_cfi(): # The Conformal Fragment Identifier at the current position, returns # null if it could not be calculated. ans = None if in_paged_mode(): for cnum in range(cols_per_screen): left = cnum * (col_and_gap + gap) right = left + col_size top, bottom = 0, scroll_viewport.height() midx = (right - left) // 2 deltax = (right - left) // 24 deltay = (bottom - top) // 24 midy = (bottom - top) // 2 yidx = 0 while True: yb, ya = midy - yidx * deltay, midy + yidx * deltay if yb <= top or ya >= bottom: break yidx += 1 xidx = 0 ys = v'[ya]' if ya is yb else v'[yb, ya]' for cury in ys: xb, xa = midx - xidx * deltax, midx + xidx * deltax if xa <= left or xb >= right: break xidx += 1 xs = v'[xa]' if xa is xb else v'[xb, xa]' for curx in xs: cfi = cfi_at_point(curx, cury) if cfi: # print('Viewport cfi:', cfi) return cfi else: try: ans = cfi_at_current() or None except: traceback.print_exc() # if ans: # print('Viewport cfi:', ans) return ans def progress_frac(frac): # The current scroll position as a fraction between 0 and 1 if in_paged_mode(): limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size() if limit <= 0: return 1 # ensures that if the book ends with a single page file the last shown percentage is 100% return current_scroll_offset() / limit # In flow mode, we scroll in the block direction, so use that limit = scroll_viewport.document_block_size() - scroll_viewport.block_size() if limit <= 0: return 1 return Math.max(0, Math.min(scroll_viewport.block_pos() / limit, 1)) def page_counts(): if in_paged_mode(): return {'current': column_at_current_scroll_offset(), 'total': number_of_cols, 'pages_per_screen': cols_per_screen} doc_size = scroll_viewport.document_block_size() screen_size = scroll_viewport.block_size() pos = scroll_viewport.block_pos() return { 'current': (pos + 10) // screen_size, 'total': doc_size // screen_size, 'pages_per_screen': 1 } def next_spine_item(backward): if not backward: csi = current_spine_item() next_spine_item.forward_scroll_data = { 'cols_per_screen': cols_per_screen, 'cols_left': number_of_cols_left(), 'spine_index': csi.index, 'spine_name': csi.name, 'current_col': column_at(current_scroll_offset() + 10) } get_boss().send_message('next_spine_item', previous=backward) def is_return(): fsd = next_spine_item.forward_scroll_data csi = current_spine_item() return fsd and fsd.cols_per_screen is cols_per_screen and fsd.spine_index is csi.index and fsd.spine_name is csi.name class HandleWheel: def __init__(self): self.reset() def reset(self): self.last_event_mode = 'page' self.last_event_at = -10000 self.last_event_backwards = False self.accumulated_scroll = 0 def onwheel(self, evt): if not evt.deltaY: return backward = evt.deltaY < 0 if evt.deltaMode is window.WheelEvent.DOM_DELTA_PIXEL: self.add_pixel_scroll(backward, Math.abs(evt.deltaY)) else: self.do_scroll(backward) def add_pixel_scroll(self, backward, deltaY): now = window.performance.now() if now - self.last_event_at > 1000 or self.last_event_backwards is not backward or self.last_event_mode is not 'pixel': self.accumulated_scroll = 0 self.last_event_mode = 'pixel' self.last_event_at = now self.last_event_backwards = backward self.accumulated_scroll += deltaY if self.accumulated_scroll > opts.paged_pixel_scroll_threshold: self.do_scroll(backward) def do_scroll(self, backward): self.reset() if opts.paged_wheel_scrolls_by_screen: pos = previous_screen_location() if backward else next_screen_location() else: pos = previous_col_location() if backward else next_col_location() if pos is -1: next_spine_item(backward) else: scroll_to_pos(pos) wheel_handler = HandleWheel() onwheel = wheel_handler.onwheel def scroll_by_page(backward, by_screen, flip_if_rtl_page_progression): if flip_if_rtl_page_progression and rtl_page_progression(): backward = not backward if by_screen: pos = previous_screen_location() if backward else next_screen_location() pages = cols_per_screen else: pos = previous_col_location() if backward else next_col_location() pages = 1 if pos is -1: # dont report human scroll since we dont know if a full page was # scrolled or not next_spine_item(backward) else: if not backward: scrolled_frac = (pages / number_of_cols) if number_of_cols > 0 else 0 get_boss().report_human_scroll(scrolled_frac) else: get_boss().report_human_scroll() scroll_to_pos(pos) def scroll_to_extend_annotation(backward): pos = previous_col_location() if backward else next_col_location() if pos is -1: return False scroll_to_pos(pos) return True def handle_shortcut(sc_name, evt): if sc_name is 'up': scroll_by_page(backward=True, by_screen=True, flip_if_rtl_page_progression=False) return True if sc_name is 'down': scroll_by_page(backward=False, by_screen=True, flip_if_rtl_page_progression=False) return True if sc_name is 'start_of_file': get_boss().report_human_scroll() scroll_to_offset(0) return True if sc_name is 'end_of_file': get_boss().report_human_scroll() scroll_to_offset(scroll_viewport.document_inline_size()) return True if sc_name is 'left': scroll_by_page(backward=True, by_screen=False, flip_if_rtl_page_progression=True) return True if sc_name is 'right': scroll_by_page(backward=False, by_screen=False, flip_if_rtl_page_progression=True) return True if sc_name is 'start_of_book': get_boss().report_human_scroll() get_boss().send_message('goto_doc_boundary', start=True) return True if sc_name is 'end_of_book': get_boss().report_human_scroll() get_boss().send_message('goto_doc_boundary', start=False) return True if sc_name is 'pageup': scroll_by_page(backward=True, by_screen=True, flip_if_rtl_page_progression=False) return True if sc_name is 'pagedown': scroll_by_page(backward=False, by_screen=True, flip_if_rtl_page_progression=False) return True if sc_name is 'toggle_autoscroll': auto_scroll_action('toggle') return True return False def handle_gesture(gesture): if gesture.type is 'swipe': if gesture.axis is 'vertical': if not gesture.active: get_boss().send_message('next_section', forward=gesture.direction is 'up') else: if not gesture.active or gesture.is_held: scroll_by_page(gesture.direction is 'right', True, flip_if_rtl_page_progression=True) # Gesture progression direction is determined in the gesture code, # don't set flip_if_rtl_page_progression=True here. elif gesture.type is 'prev-page': scroll_by_page(True, opts.paged_taps_scroll_by_screen, flip_if_rtl_page_progression=False) elif gesture.type is 'next-page': scroll_by_page(False, opts.paged_taps_scroll_by_screen, flip_if_rtl_page_progression=False) anchor_funcs = { 'pos_for_elem': def pos_for_elem(elem): if not elem: return 0 elem = scrollable_element(elem) br = elem.getBoundingClientRect() pos = scroll_viewport.viewport_to_document_inline( scroll_viewport.rect_inline_start(br)) return column_at(pos) , 'visibility': def visibility(pos): first = column_at(current_scroll_offset() + 10) if pos < first: return -1 if pos < first + cols_per_screen: return 0 return 1 , 'cmp': def cmp(a, b): return a - b , } class ResizeManager: def __init__(self): self.reset() def reset(self): self.resize_in_progress = None self.last_transition = None def start_resize(self, width, height): self.resize_in_progress = {'width': width, 'height': height, 'column': last_scrolled_to_column} def end_resize(self): if not self.resize_in_progress: return rp, self.resize_in_progress = self.resize_in_progress, None transition = {'before': rp, 'after': { 'width': scroll_viewport.width(), 'height': scroll_viewport.height(), 'column': last_scrolled_to_column}} if self.is_inverse_transition(transition): if transition.after.column is not self.last_transition.before.column: scroll_to_column(transition.after.column) transition.after.column = last_scrolled_to_column self.last_transition = transition def is_inverse_transition(self, transition): p = self.last_transition if not p: return False p = p.after n = transition.before return p.column is n.column and p.width is n.width and p.height is n.height resize_manager = ResizeManager() def prepare_for_resize(width, height): resize_manager.start_resize(width, height) def resize_done(): resize_manager.end_resize() def auto_scroll_action(action): if action is 'toggle': get_boss().send_message('error', errkey='no-auto-scroll-in-paged-mode', is_non_critical=True) return False class DragScroller: INTERVAL = 500 def __init__(self): self.backward = False self.timer_id = None def is_running(self): return self.timer_id is not None def start(self, backward): if not self.is_running() or backward is not self.backward: self.stop() self.backward = backward self.timer_id = window.setTimeout(self.do_one_page_turn, self.INTERVAL) def do_one_page_turn(self): pos = previous_col_location() if self.backward else next_col_location() if pos >= 0: scroll_to_pos(pos) self.timer_id = window.setTimeout(self.do_one_page_turn, self.INTERVAL * 2) else: self.stop() def stop(self): if self.timer_id is not None: window.clearTimeout(self.timer_id) self.timer_id = None drag_scroller = DragScroller() def cancel_drag_scroll(): drag_scroller.stop() def start_drag_scroll(delta): drag_scroller.start(delta < 0)