calibre/src/pyj/read_book/paged_mode.pyj
luz paz df439bedd1 FIx typos
Found via `codespell -q 3 -S./Changelog.*,./resources/dictionaries  -L alo,ans,pard,ro`
2022-07-18 21:52:03 -04:00

936 lines
36 KiB
Plaintext

# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
# 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 <div>. 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
# <div>, 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. Detect 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)