mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Vertical RTL Book Reading Support
Added a variety of functions to viewport to allow working based on block and inline direction rather than X and Y. Changed paged mode and flow mode to be based on block and inline directions, making them agnostic to writing direction. Added jump_to_cfi function to flow mode code, and use this in iframe. This fixes some issues with CFI jumping in flow mode. Use self.jump_to_cfi in iframe so that it is based on the current mode. Removed a redundant self.onscroll call that was causing CFIs to be queried twice on load, doubling the time they took. Fixed some bugs related to scrolling in flow mode related to a decorator not working with keyword arguments, by removing the keyword arguments. Fixed some bugs with flow mode visibility anchor function. CFI changes: Renamed some functions and variables to clarify meaning. Remove use of exceptions, since they were causing CFI calculation to run about six times more slowly in benchmarks. (Worst-case went from ~6 to ~1 second.) Remove forward flag and allow text offset to be equal to node length, and correct for this by adding a flag to the decoded CFI to indicate this. Added comments clarifying the results of decoding a CFI. Fix bugs in the point (now decode_with_range) function and simplified it. Added new functions that get CFI position from decoded CFIs, to replace places where this was being done in slightly different ways. Fixed some issues with cfi.pyj's scroll_to that were exposed by vertical writing support, mainly caching and using span's parent to create the start and end ranges, which causes re-creating the range to succeed. Also, don't store the span's bounding rect, since it's not needed anymore. Rewrote at_current to do a simplified scan of the viewport in the inline and then block direction.
This commit is contained in:
parent
c5b9677809
commit
8b39fea8f8
@ -57,6 +57,9 @@ class Parser(object):
|
|||||||
def parse_epubcfi(self, raw):
|
def parse_epubcfi(self, raw):
|
||||||
' Parse a full epubcfi of the form epubcfi(path [ , path , path ]) '
|
' Parse a full epubcfi of the form epubcfi(path [ , path , path ]) '
|
||||||
null = {}, {}, {}, raw
|
null = {}, {}, {}, raw
|
||||||
|
if not raw:
|
||||||
|
return null
|
||||||
|
|
||||||
if not raw.startswith('epubcfi('):
|
if not raw.startswith('epubcfi('):
|
||||||
return null
|
return null
|
||||||
raw = raw[len('epubcfi('):]
|
raw = raw[len('epubcfi('):]
|
||||||
|
@ -136,11 +136,15 @@ absolute_font_sizes = {
|
|||||||
'medium': '1rem',
|
'medium': '1rem',
|
||||||
'large': '1.125rem', 'x-large': '1.5rem', 'xx-large': '2rem', 'xxx-large': '2.55rem'
|
'large': '1.125rem', 'x-large': '1.5rem', 'xx-large': '2rem', 'xxx-large': '2.55rem'
|
||||||
}
|
}
|
||||||
|
nonstandard_writing_mode_property_names = ('-webkit-writing-mode', '-epub-writing-mode')
|
||||||
|
|
||||||
|
|
||||||
def transform_declaration(decl):
|
def transform_declaration(decl):
|
||||||
decl = StyleDeclaration(decl)
|
decl = StyleDeclaration(decl)
|
||||||
changed = False
|
changed = False
|
||||||
|
nonstandard_writing_mode_props = {}
|
||||||
|
standard_writing_mode_props = {}
|
||||||
|
|
||||||
for prop, parent_prop in tuple(decl):
|
for prop, parent_prop in tuple(decl):
|
||||||
if prop.name in page_break_properties:
|
if prop.name in page_break_properties:
|
||||||
changed = True
|
changed = True
|
||||||
@ -162,6 +166,18 @@ def transform_declaration(decl):
|
|||||||
changed = True
|
changed = True
|
||||||
l = convert_fontsize(l, unit)
|
l = convert_fontsize(l, unit)
|
||||||
decl.change_property(prop, parent_prop, unicode_type(l) + 'rem')
|
decl.change_property(prop, parent_prop, unicode_type(l) + 'rem')
|
||||||
|
elif prop.name in nonstandard_writing_mode_property_names:
|
||||||
|
nonstandard_writing_mode_props[prop.value] = prop.priority
|
||||||
|
elif prop.name == 'writing-mode':
|
||||||
|
standard_writing_mode_props[prop.value] = True
|
||||||
|
|
||||||
|
# Add standard writing-mode properties if they don't exist so that
|
||||||
|
# all of the browsers supported by the viewer work in vertical modes
|
||||||
|
for value, priority in nonstandard_writing_mode_props.items():
|
||||||
|
if value not in standard_writing_mode_props:
|
||||||
|
decl.set_property('writing-mode', value, priority)
|
||||||
|
changed = True
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ from __python__ import hash_literals
|
|||||||
# scroll_to(cfi): which scrolls the browser to a point corresponding to the
|
# scroll_to(cfi): which scrolls the browser to a point corresponding to the
|
||||||
# given cfi, and returns the x and y co-ordinates of the point.
|
# given cfi, and returns the x and y co-ordinates of the point.
|
||||||
|
|
||||||
from read_book.viewport import scroll_viewport
|
from read_book.viewport import scroll_viewport, rem_size
|
||||||
|
|
||||||
# CFI escaping {{{
|
# CFI escaping {{{
|
||||||
escape_pat = /[\[\],^();~@!-]/g
|
escape_pat = /[\[\],^();~@!-]/g
|
||||||
@ -52,11 +52,6 @@ def get_current_time(target): # {{{
|
|||||||
return fstr(target.currentTime or 0)
|
return fstr(target.currentTime or 0)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def window_scroll_pos(w): # {{{
|
|
||||||
w = w or window
|
|
||||||
return w.pageXOffset, w.pageYOffset
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
# Convert point to character offset {{{
|
# Convert point to character offset {{{
|
||||||
def range_has_point(range_, x, y):
|
def range_has_point(range_, x, y):
|
||||||
rects = range_.getClientRects()
|
rects = range_.getClientRects()
|
||||||
@ -102,7 +97,7 @@ def find_offset_for_point(x, y, node, cdoc):
|
|||||||
|
|
||||||
# The point must be after the last bit of text/in the padding/border, we dont know
|
# The point must be after the last bit of text/in the padding/border, we dont know
|
||||||
# how to get a good point in this case
|
# how to get a good point in this case
|
||||||
raise ValueError(str.format("Point ({}, {}) is in the padding/border of the node, so cannot calculate offset", x, y))
|
return None, None
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -279,7 +274,7 @@ def node_for_path_step(parent, target, assertion):
|
|||||||
return node_at_index(parent.childNodes, target, 0, not is_element)[0]
|
return node_at_index(parent.childNodes, target, 0, not is_element)[0]
|
||||||
|
|
||||||
|
|
||||||
def node_for_text_offset(nodes, offset, forward, first_node):
|
def node_for_text_offset(nodes, offset, first_node):
|
||||||
last_text_node = None
|
last_text_node = None
|
||||||
seen_first = False
|
seen_first = False
|
||||||
for i in range(nodes.length):
|
for i in range(nodes.length):
|
||||||
@ -291,17 +286,30 @@ def node_for_text_offset(nodes, offset, forward, first_node):
|
|||||||
continue
|
continue
|
||||||
if is_text_node(node):
|
if is_text_node(node):
|
||||||
l = node.nodeValue.length
|
l = node.nodeValue.length
|
||||||
if offset < l or (not forward and offset is l):
|
if offset <= l:
|
||||||
return node, offset, True
|
return node, offset, True
|
||||||
last_text_node = node
|
last_text_node = node
|
||||||
offset -= l
|
offset -= l
|
||||||
elif node.nodeType is Node.ELEMENT_NODE and node.dataset.calibreRangeWrapper:
|
elif node.nodeType is Node.ELEMENT_NODE and node.dataset.calibreRangeWrapper:
|
||||||
qn, offset, ok = node_for_text_offset(unwrapped_nodes(node), offset, forward)
|
qn, offset, ok = node_for_text_offset(unwrapped_nodes(node), offset)
|
||||||
if ok:
|
if ok:
|
||||||
return qn, offset, True
|
return qn, offset, True
|
||||||
return last_text_node, offset, False
|
return last_text_node, offset, False
|
||||||
|
|
||||||
|
# Based on a CFI string, returns a decoded CFI, with members:
|
||||||
|
#
|
||||||
|
# node: The node to which the CFI refers.
|
||||||
|
# time: If the CFI refers to a video or sound, this is the time within such to which it refers.
|
||||||
|
# x, y: If the CFI defines a spacial offset (technically only valid for images and videos),
|
||||||
|
# these are the X and Y percentages from the top-left of the image or video.
|
||||||
|
# Note that Calibre has a fallback to set CFIs with spacial offset on the HTML document,
|
||||||
|
# and interprets them as a position within the Calibre-rendered document.
|
||||||
|
# forward: This is set to True if the CFI had a side bias of 'a' (meaning 'after').
|
||||||
|
# offset: When the CFI refers to a text node, this is the offset (zero-based index) of the character
|
||||||
|
# the CFI refers to. The position is defined as being before the specified character,
|
||||||
|
# i.e. an offset of 0 is before the first character and an offset equal to number of characters
|
||||||
|
# in the text is after the last character.
|
||||||
|
# error: If there was a problem decoding the CFI, this is set to a string describing the error.
|
||||||
def decode(cfi, doc):
|
def decode(cfi, doc):
|
||||||
doc = doc or window.document
|
doc = doc or window.document
|
||||||
simple_node_regex = ///
|
simple_node_regex = ///
|
||||||
@ -341,7 +349,7 @@ def decode(cfi, doc):
|
|||||||
print(error)
|
print(error)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
point = {}
|
decoded = {}
|
||||||
error = None
|
error = None
|
||||||
offset = None
|
offset = None
|
||||||
|
|
||||||
@ -354,14 +362,14 @@ def decode(cfi, doc):
|
|||||||
r = cfi.match(/^~(-?\d+(\.\d+)?)/)
|
r = cfi.match(/^~(-?\d+(\.\d+)?)/)
|
||||||
if r:
|
if r:
|
||||||
# Temporal offset
|
# Temporal offset
|
||||||
point.time = r[1] - 0 # Coerce to number
|
decoded.time = r[1] - 0 # Coerce to number
|
||||||
cfi = cfi.substr(r[0].length)
|
cfi = cfi.substr(r[0].length)
|
||||||
|
|
||||||
r = cfi.match(/^@(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)/)
|
r = cfi.match(/^@(-?\d+(\.\d+)?):(-?\d+(\.\d+)?)/)
|
||||||
if r:
|
if r:
|
||||||
# Spatial offset
|
# Spatial offset
|
||||||
point.x = r[1] - 0 # Coerce to number
|
decoded.x = r[1] - 0 # Coerce to number
|
||||||
point.y = r[3] - 0 # Coerce to number
|
decoded.y = r[3] - 0 # Coerce to number
|
||||||
cfi = cfi.substr(r[0].length)
|
cfi = cfi.substr(r[0].length)
|
||||||
|
|
||||||
r = cfi.match(/^\[([^\]]+)\]/)
|
r = cfi.match(/^\[([^\]]+)\]/)
|
||||||
@ -372,7 +380,7 @@ def decode(cfi, doc):
|
|||||||
if r:
|
if r:
|
||||||
if r.index > 0 and assertion[r.index - 1] is not '^':
|
if r.index > 0 and assertion[r.index - 1] is not '^':
|
||||||
assertion = assertion.substr(0, r.index)
|
assertion = assertion.substr(0, r.index)
|
||||||
point.forward = (r[1] is 'a')
|
decoded.forward = (r[1] is 'a')
|
||||||
assertion = unescape_from_cfi(assertion)
|
assertion = unescape_from_cfi(assertion)
|
||||||
# TODO: Handle text assertion
|
# TODO: Handle text assertion
|
||||||
|
|
||||||
@ -381,21 +389,21 @@ def decode(cfi, doc):
|
|||||||
orig_offset = offset
|
orig_offset = offset
|
||||||
if node.parentNode?.nodeType is Node.ELEMENT_NODE and node.parentNode.dataset.calibreRangeWrapper:
|
if node.parentNode?.nodeType is Node.ELEMENT_NODE and node.parentNode.dataset.calibreRangeWrapper:
|
||||||
node = node.parentNode
|
node = node.parentNode
|
||||||
node, offset, ok = node_for_text_offset(node.parentNode.childNodes, offset, point.forward, node)
|
node, offset, ok = node_for_text_offset(node.parentNode.childNodes, offset, node)
|
||||||
if not ok:
|
if not ok:
|
||||||
error = "Offset out of range: " + orig_offset
|
error = "Offset out of range: " + orig_offset
|
||||||
point.offset = offset
|
decoded.offset = offset
|
||||||
|
|
||||||
point.node = node
|
decoded.node = node
|
||||||
if error:
|
if error:
|
||||||
point.error = error
|
decoded.error = error
|
||||||
else if cfi.length > 0:
|
else if cfi.length > 0:
|
||||||
point.error = "Undecoded CFI: " + cfi
|
decoded.error = "Undecoded CFI: " + cfi
|
||||||
|
|
||||||
if point.error:
|
if decoded.error:
|
||||||
print(point.error)
|
print(decoded.error)
|
||||||
|
|
||||||
return point
|
return decoded
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def cfi_sort_key(cfi): # {{{
|
def cfi_sort_key(cfi): # {{{
|
||||||
@ -536,85 +544,169 @@ def at(x, y, doc): # {{{
|
|||||||
# caretRangeFromPoint does weird things when the point falls in the
|
# caretRangeFromPoint does weird things when the point falls in the
|
||||||
# padding of the element
|
# padding of the element
|
||||||
target, offset = find_offset_for_point(x, y, target, cdoc)
|
target, offset = find_offset_for_point(x, y, target, cdoc)
|
||||||
|
if target is None:
|
||||||
|
return None
|
||||||
|
|
||||||
return encode(doc, target, offset, tail)
|
return encode(doc, target, offset, tail)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def point(cfi, doc): # {{{
|
# Like decode(), but tries to construct a range from the CFI's character offset and include it in
|
||||||
|
# the return value.
|
||||||
|
#
|
||||||
|
# If the CFI defines a character offset, there are three cases:
|
||||||
|
# Case 1. If the offset is 0 and the text node's length is zero,
|
||||||
|
# the range is set to None. This is a failure case, but
|
||||||
|
# later code will try to use the node's bounding box.
|
||||||
|
# Case 2. Otherwise, if the offset is equal to the length of the range,
|
||||||
|
# then a range from the previous to the last character is created,
|
||||||
|
# and use_range_end_pos is set. This is the special case.
|
||||||
|
# Case 3. Otherwise, the range is set start at the offset and end at one character past the offset.
|
||||||
|
#
|
||||||
|
# In cases 2 and 3, the range is then checked to verify that bounding information can be obtained.
|
||||||
|
# If not, no range is returned.
|
||||||
|
#
|
||||||
|
# If the CFI does not define a character offset, then the spacial offset is set in the return value.
|
||||||
|
#
|
||||||
|
# Includes everything that the decode() function does, in addition to:
|
||||||
|
# range: A text range, as desribed above.
|
||||||
|
# use_range_end_pos: If this is True, a position calculated from the range should
|
||||||
|
# use the position after the last character in the range.
|
||||||
|
# (This is set if the offset is equal to the length of the text in the node.)
|
||||||
|
def decode_with_range(cfi, doc): # {{{
|
||||||
doc = doc or window.document
|
doc = doc or window.document
|
||||||
r = decode(cfi, doc)
|
decoded = decode(cfi, doc)
|
||||||
if not r:
|
if not decoded:
|
||||||
return None
|
return None
|
||||||
node = r.node
|
node = decoded.node
|
||||||
ndoc = node.ownerDocument
|
ndoc = node.ownerDocument
|
||||||
if not ndoc:
|
if not ndoc:
|
||||||
print(str.format("CFI node has no owner document: {} {}", cfi, node))
|
print(str.format("CFI node has no owner document: {} {}", cfi, node))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
x = None
|
|
||||||
y = None
|
|
||||||
range_ = None
|
range_ = None
|
||||||
|
position_at_end_of_range = None
|
||||||
|
|
||||||
if jstype(r.offset) is "number":
|
if jstype(decoded.offset) is "number":
|
||||||
# Character offset
|
# We can only create a meaningful range if the node length is
|
||||||
range_ = ndoc.createRange()
|
# positive and nonzero.
|
||||||
if r.forward:
|
|
||||||
try_list = [{'start':0, 'end':0, 'a':0.5}, {'start':0, 'end':1, 'a':1}, {'start':-1, 'end':0, 'a':0}]
|
|
||||||
else:
|
|
||||||
try_list = [{'start':0, 'end':0, 'a':0.5}, {'start':-1, 'end':0, 'a':0}, {'start':0, 'end':1, 'a':1}]
|
|
||||||
a = None
|
|
||||||
node_len = node.nodeValue.length if node.nodeValue else 0
|
node_len = node.nodeValue.length if node.nodeValue else 0
|
||||||
offset = r.offset
|
if node_len:
|
||||||
if not offset:
|
range_ = ndoc.createRange()
|
||||||
range_.setStart(node, 0)
|
|
||||||
range_.setEnd(node, 0)
|
|
||||||
else:
|
|
||||||
rects = v'[]'
|
|
||||||
for v'var i = 0; i < 2; i++':
|
|
||||||
# Try reducing the offset by 1 if we get no match as if it refers to the position after the
|
|
||||||
# last character we wont get a match with getClientRects
|
|
||||||
offset = r.offset - i
|
|
||||||
if offset < 0:
|
|
||||||
offset = 0
|
|
||||||
k = 0
|
|
||||||
while not rects?.length and k < try_list.length:
|
|
||||||
t = try_list[k]
|
|
||||||
k += 1
|
|
||||||
start_offset = offset + t.start
|
|
||||||
end_offset = offset + t.end
|
|
||||||
a = t.a
|
|
||||||
if start_offset < 0 or end_offset >= node_len:
|
|
||||||
continue
|
|
||||||
range_.setStart(node, start_offset)
|
|
||||||
range_.setEnd(node, end_offset)
|
|
||||||
rects = range_.getClientRects()
|
|
||||||
|
|
||||||
if not rects?.length:
|
# Check for special case: End of range offset, after the last character
|
||||||
print(str.format("Could not find caret position for {} : rects: {} offset: {}", cfi, rects, r.offset))
|
offset = decoded.offset
|
||||||
return None
|
position_at_end_of_range = False
|
||||||
|
if offset == node_len:
|
||||||
|
offset -= 1
|
||||||
|
position_at_end_of_range = True
|
||||||
|
|
||||||
else:
|
range_.setStart(node, offset)
|
||||||
x, y = r.x, r.y
|
range_.setEnd(node, offset + 1)
|
||||||
|
rect = range_.getBoundingClientRect()
|
||||||
|
|
||||||
return {'x':x, 'y':y, 'node':r.node, 'time':r.time, 'range':range_, 'a':a}
|
if not rect:
|
||||||
|
print(str.format("Could not find caret position for {} (offset: {})", cfi, decoded.offset))
|
||||||
|
range_ = None
|
||||||
|
|
||||||
|
# Augment decoded with range, if found
|
||||||
|
decoded.range = range_
|
||||||
|
decoded.use_range_end_pos = position_at_end_of_range
|
||||||
|
|
||||||
|
return decoded
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def scroll_to(cfi, callback, doc): # {{{
|
# It's only valid to call this if you have a decoded CFI with a range included.
|
||||||
doc = doc or window.doc
|
# Call decoded_to_document_position if you're not sure.
|
||||||
point_ = point(cfi, doc)
|
def decoded_range_to_document_position(decoded):
|
||||||
if not point_:
|
# Character offset
|
||||||
print("No point found for cfi: " + cfi)
|
# Get the bounding rect of the range, in (real) viewport space
|
||||||
return
|
rect = decoded.range.getBoundingClientRect()
|
||||||
if jstype(point_.time) is 'number':
|
|
||||||
set_current_time(point_.node, point_.time)
|
|
||||||
|
|
||||||
if point_.range is not None:
|
# Now, get the viewport-space position (vs_pos) we want to scroll to
|
||||||
|
# This is a little complicated.
|
||||||
|
# The box we are using is a character's bounding box.
|
||||||
|
# First, the inline direction.
|
||||||
|
# We want to use the beginning of the box per the ePub CFI spec,
|
||||||
|
# which states character offset CFIs refer to the beginning of the
|
||||||
|
# character, which would be in the inline direction.
|
||||||
|
inline_vs_pos = scroll_viewport.rect_inline_start(rect)
|
||||||
|
# Unless the flag is set to use the end of the range:
|
||||||
|
# (because ranges indices may not exceed the range,
|
||||||
|
# but CFI offets are allowed to be one past the end.)
|
||||||
|
if decoded.use_range_end_pos:
|
||||||
|
inline_vs_pos = scroll_viewport.rect_inline_end(rect)
|
||||||
|
|
||||||
|
# Next, the block direction.
|
||||||
|
# If the CFI specifies a side bias, we should respect that.
|
||||||
|
# Otherwise, we want to use the block start.
|
||||||
|
if decoded.forward:
|
||||||
|
block_vs_pos = scroll_viewport.rect_block_end(rect)
|
||||||
|
else:
|
||||||
|
block_vs_pos = scroll_viewport.rect_block_start(rect)
|
||||||
|
|
||||||
|
# Now, we need to convert these to document X and Y coordinates.
|
||||||
|
return scroll_viewport.viewport_to_document_inline_block(inline_vs_pos, block_vs_pos, decoded.node.ownerDocument)
|
||||||
|
|
||||||
|
# This will work on a decoded CFI that refers to a node or node with spacial offset.
|
||||||
|
# It will ignore any ranges, so call decoded_to_document_position unless you're sure the decoded CFI has no range.
|
||||||
|
def decoded_node_or_spacial_offset_to_document_position(decoded):
|
||||||
|
node = decoded.node
|
||||||
|
rect = node.getBoundingClientRect()
|
||||||
|
percentx, percenty = decoded.x, decoded.y
|
||||||
|
# If we have a spacial offset, base the CFI position on the offset into the object
|
||||||
|
if jstype(percentx) is 'number' and node.offsetWidth and jstype(percenty) is 'number' and node.offsetHeight:
|
||||||
|
viewx = rect.left + (percentx*node.offsetWidth)/100
|
||||||
|
viewy = rect.top + (percenty*node.offsetHeight)/100
|
||||||
|
doc_left, doc_top = scroll_viewport.viewport_to_document(viewx, viewy, node.ownerDocument)
|
||||||
|
return doc_left, doc_top
|
||||||
|
# Otherwise, base it on the inline and block start and end, along with side bias:
|
||||||
|
else:
|
||||||
|
if decoded.forward:
|
||||||
|
block_vs_pos = scroll_viewport.rect_block_end(rect)
|
||||||
|
else:
|
||||||
|
block_vs_pos = scroll_viewport.rect_block_start(rect)
|
||||||
|
|
||||||
|
inline_vs_pos = scroll_viewport.rect_inline_start(rect)
|
||||||
|
return scroll_viewport.viewport_to_document_inline_block(inline_vs_pos, block_vs_pos, node.ownerDocument)
|
||||||
|
|
||||||
|
# This will take a decoded CFI and return a document position.
|
||||||
|
#
|
||||||
|
def decoded_to_document_position(decoded):
|
||||||
|
node = decoded.node
|
||||||
|
if decoded.range is not None:
|
||||||
|
return decoded_range_to_document_position(decoded)
|
||||||
|
else if node is not None and node.getBoundingClientRect:
|
||||||
|
return decoded_node_or_spacial_offset_to_document_position(decoded)
|
||||||
|
# No range, so we can't use that, and no node, so any spacial offset is meaningless
|
||||||
|
else:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def scroll_to(cfi, callback, doc): # {{{
|
||||||
|
decoded = decode_with_range(cfi, doc)
|
||||||
|
if not decoded:
|
||||||
|
print("No information found for cfi: " + cfi)
|
||||||
|
return
|
||||||
|
if jstype(decoded.time) is 'number':
|
||||||
|
set_current_time(decoded.node, decoded.time)
|
||||||
|
|
||||||
|
if decoded.range is not None:
|
||||||
# Character offset
|
# Character offset
|
||||||
r = point_.range
|
r = decoded.range
|
||||||
so, eo, sc, ec = r.startOffset, r.endOffset, r.startContainer, r.endContainer
|
so, eo = r.startOffset, r.endOffset
|
||||||
node = r.startContainer
|
original_node = r.startContainer
|
||||||
ndoc = node.ownerDocument
|
|
||||||
|
# Save the node index and its parent so we can get the node back
|
||||||
|
# after removing the span and normalizing the parent.
|
||||||
|
node_parent = original_node.parentNode
|
||||||
|
original_node_index = 0
|
||||||
|
for child_node in node_parent.childNodes:
|
||||||
|
if child_node is original_node:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
++original_node_index
|
||||||
|
|
||||||
|
ndoc = original_node.ownerDocument
|
||||||
span = ndoc.createElement('span')
|
span = ndoc.createElement('span')
|
||||||
span.setAttribute('style', 'border-width: 0; padding: 0; margin: 0')
|
span.setAttribute('style', 'border-width: 0; padding: 0; margin: 0')
|
||||||
r.surroundContents(span)
|
r.surroundContents(span)
|
||||||
@ -622,15 +714,6 @@ def scroll_to(cfi, callback, doc): # {{{
|
|||||||
fn = def():
|
fn = def():
|
||||||
# Remove the span and get the new position now that scrolling
|
# Remove the span and get the new position now that scrolling
|
||||||
# has (hopefully) completed
|
# has (hopefully) completed
|
||||||
#
|
|
||||||
# In WebKit, the boundingrect of the span is wrong in some
|
|
||||||
# situations, whereas in IE resetting the range causes it to
|
|
||||||
# loose bounding info. So we use the range's rects unless they
|
|
||||||
# are absent, in which case we use the span's rect
|
|
||||||
#
|
|
||||||
rect = span.getBoundingClientRect()
|
|
||||||
|
|
||||||
# Remove the span we inserted
|
|
||||||
p = span.parentNode
|
p = span.parentNode
|
||||||
for node in span.childNodes:
|
for node in span.childNodes:
|
||||||
span.removeChild(node)
|
span.removeChild(node)
|
||||||
@ -642,7 +725,7 @@ def scroll_to(cfi, callback, doc): # {{{
|
|||||||
offset = so
|
offset = so
|
||||||
while offset > -1:
|
while offset > -1:
|
||||||
try:
|
try:
|
||||||
r.setStart(sc, offset)
|
decoded.range.setStart(node_parent.childNodes[original_node_index], offset)
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
offset -= 1
|
offset -= 1
|
||||||
@ -650,36 +733,49 @@ def scroll_to(cfi, callback, doc): # {{{
|
|||||||
offset = eo
|
offset = eo
|
||||||
while offset > -1:
|
while offset > -1:
|
||||||
try:
|
try:
|
||||||
r.setEnd(ec, offset)
|
decoded.range.setEnd(node_parent.childNodes[original_node_index], offset)
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
offset -= 1
|
offset -= 1
|
||||||
|
|
||||||
rects = r.getClientRects()
|
doc_x, doc_y = decoded_range_to_document_position(decoded)
|
||||||
if rects.length > 0:
|
|
||||||
rect = rects[0]
|
# Abort if CFI position is invalid
|
||||||
|
if doc_x is None or doc_y is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Since this position will be used for the upper-left of the viewport,
|
||||||
|
# in RTL mode, we need to offset it by the viewport width so the
|
||||||
|
# position will be on the right side of the viewport,
|
||||||
|
# from where we start reading.
|
||||||
|
# (Note that adding moves left in RTL mode because of the viewport X
|
||||||
|
# coordinate reversal in RTL mode.)
|
||||||
|
if scroll_viewport.rtl:
|
||||||
|
doc_x += scroll_viewport.width()
|
||||||
|
|
||||||
x = (point_.a*rect.left + (1-point_.a)*rect.right)
|
|
||||||
y = (rect.top + rect.bottom)/2
|
|
||||||
x, y = scroll_viewport.viewport_to_document(x, y, ndoc)
|
|
||||||
if callback:
|
if callback:
|
||||||
callback(x, y)
|
callback(doc_x, doc_y)
|
||||||
else:
|
else:
|
||||||
node = point_.node
|
node = decoded.node
|
||||||
scroll_viewport.scroll_into_view(node)
|
scroll_viewport.scroll_into_view(node)
|
||||||
|
|
||||||
fn = def():
|
fn = def():
|
||||||
r = node.getBoundingClientRect()
|
doc_x, doc_y = decoded_node_or_spacial_offset_to_document_position(decoded)
|
||||||
# Start of element is right side in RTL, so be sure to get that side in RTL mode
|
|
||||||
x, y = scroll_viewport.viewport_to_document(
|
# Abort if CFI position is invalid
|
||||||
r.left if scroll_viewport.ltr else r.right, r.top, node.ownerDocument)
|
if doc_x is None or doc_y is None:
|
||||||
if jstype(point_.x) is 'number' and node.offsetWidth:
|
return
|
||||||
x += (point_.x*node.offsetWidth)/100
|
|
||||||
if jstype(point_.y) is 'number' and node.offsetHeight:
|
# Since this position will be used for the upper-left of the viewport,
|
||||||
y += (point_.y*node.offsetHeight)/100
|
# in RTL mode, we need to offset it by the viewport width so the
|
||||||
scroll_viewport.scroll_to(x, y)
|
# position will be on the right side of the viewport,
|
||||||
|
# from where we start reading.
|
||||||
|
# (Note that adding moves left in RTL mode because of the viewport X
|
||||||
|
# coordinate reversal in RTL mode.)
|
||||||
|
if scroll_viewport.rtl:
|
||||||
|
doc_x += scroll_viewport.width()
|
||||||
if callback:
|
if callback:
|
||||||
callback(x, y)
|
callback(doc_x, doc_y)
|
||||||
|
|
||||||
setTimeout(fn, 10)
|
setTimeout(fn, 10)
|
||||||
|
|
||||||
@ -694,74 +790,108 @@ def at_point(ox, oy): # {{{
|
|||||||
def dist(p1, p2):
|
def dist(p1, p2):
|
||||||
Math.sqrt(Math.pow(p1[0]-p2[0], 2), Math.pow(p1[1]-p2[1], 2))
|
Math.sqrt(Math.pow(p1[0]-p2[0], 2), Math.pow(p1[1]-p2[1], 2))
|
||||||
|
|
||||||
try:
|
cfi = at(ox, oy)
|
||||||
cfi = at(ox, oy)
|
if cfi is None:
|
||||||
p = point(cfi)
|
return None
|
||||||
except Exception:
|
|
||||||
cfi = None
|
|
||||||
|
|
||||||
if not p:
|
decoded = decode_with_range(cfi)
|
||||||
|
if not decoded:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if cfi:
|
if cfi:
|
||||||
if p.range is not None:
|
cfix, cfiy = decoded_to_document_position(decoded)
|
||||||
r = p.range
|
if cfix is None or cfiy is None or dist(scroll_viewport.viewport_to_document(ox, oy), v'[cfix, cfiy]') > 50:
|
||||||
rect = r.getClientRects()[0]
|
|
||||||
|
|
||||||
x = (p.a*rect.left + (1-p.a)*rect.right)
|
|
||||||
y = (rect.top + rect.bottom)/2
|
|
||||||
x, y = scroll_viewport.viewport_to_document(x, y, r.startContainer.ownerDocument)
|
|
||||||
else:
|
|
||||||
node = p.node
|
|
||||||
r = node.getBoundingClientRect()
|
|
||||||
# Start of element is right side in RTL, so be sure to get that side in RTL mode
|
|
||||||
x, y = scroll_viewport.viewport_to_document(
|
|
||||||
r.left if scroll_viewport.ltr else r.right, r.top, node.ownerDocument)
|
|
||||||
if jstype(p.x) is 'number' and node.offsetWidth:
|
|
||||||
x += (p.x*node.offsetWidth)/100
|
|
||||||
if jstype(p.y) is 'number' and node.offsetHeight:
|
|
||||||
y += (p.y*node.offsetHeight)/100
|
|
||||||
|
|
||||||
if dist(scroll_viewport.viewport_to_document(ox, oy), v'[x, y]') > 50:
|
|
||||||
cfi = None
|
cfi = None
|
||||||
|
|
||||||
return cfi
|
return cfi
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def at_current(): # {{{
|
def at_current(): # {{{
|
||||||
winx, winy = window_scroll_pos()
|
wini, winb = scroll_viewport.inline_size(), scroll_viewport.block_size()
|
||||||
winw, winh = scroll_viewport.width(), scroll_viewport.height()
|
|
||||||
winw = max(winw, 400)
|
|
||||||
winh = max(winh, 600)
|
|
||||||
deltay = Math.floor(winh/50)
|
|
||||||
deltax = Math.floor(winw/25)
|
|
||||||
miny = max(-winy, -winh)
|
|
||||||
maxy = winh
|
|
||||||
minx = max(-winx, -winw)
|
|
||||||
maxx = winw
|
|
||||||
|
|
||||||
def x_loop(cury):
|
# We subtract one because the the actual position query for CFI elements is relative to the
|
||||||
for direction in v'[-1, 1]':
|
# viewport, and the viewport coordinates actually go from 0 to the size - 1.
|
||||||
delta = deltax * direction
|
# If we don't do this, the first line of queries will always fail in those cases where we
|
||||||
curx = 0
|
# start at the right of the viewport because they'll be outside the viewport
|
||||||
while not ((direction < 0 and curx < minx) or (direction > 0 and curx > maxx)):
|
wini -= 1
|
||||||
cfi = at_point(curx, cury)
|
winb -= 1
|
||||||
if cfi:
|
|
||||||
return cfi
|
|
||||||
curx += delta
|
|
||||||
|
|
||||||
for direction in v'[-1, 1]':
|
# Don't let the deltas go below 10 or above 30, to prevent perverse cases where
|
||||||
delta = deltay * direction
|
# we don't loop at all, loop too many times, or skip elements because we're
|
||||||
cury = 0
|
# looping too fast.
|
||||||
while not( (direction < 0 and cury < miny) or (direction > 0 and cury > maxy) ):
|
deltai = min(max(Math.ceil(rem_size() / 2), 5), 30)
|
||||||
cfi = x_loop(cury, -1)
|
deltab = min(max(Math.ceil(rem_size() / 2), 5), 30)
|
||||||
|
|
||||||
|
# i.e. English, Hebrew:
|
||||||
|
if scroll_viewport.horizontal_writing_mode:
|
||||||
|
# Horizontal languages always start at the top
|
||||||
|
startb = 0
|
||||||
|
endb = winb
|
||||||
|
if scroll_viewport.ltr:
|
||||||
|
# i.e. English: we scan from the left margin to the right margin
|
||||||
|
starti = 0
|
||||||
|
endi = wini
|
||||||
|
else:
|
||||||
|
# i.e. Hebrew: we scan from the right margin to the left margin
|
||||||
|
starti = wini
|
||||||
|
endi = 0
|
||||||
|
deltai = -deltai
|
||||||
|
# i.e. Japanese, Mongolian script, traditional Chinese
|
||||||
|
else:
|
||||||
|
# These languages are only top-to-bottom
|
||||||
|
starti = 0
|
||||||
|
endi = winb
|
||||||
|
# i.e. Japanese: To the next line is from the right margin to the left margin
|
||||||
|
if scroll_viewport.rtl:
|
||||||
|
startb = winb
|
||||||
|
endb = 0
|
||||||
|
deltab = -deltab
|
||||||
|
# i.e. Mongolian: To the next line is from the left margin to the right margin
|
||||||
|
else:
|
||||||
|
startb = 0
|
||||||
|
endb = winb
|
||||||
|
|
||||||
|
# print(f'{starti} {deltai} {endi} {startb} {deltab} {endb}')
|
||||||
|
# Find the bounds
|
||||||
|
up_boundb = max(startb, endb)
|
||||||
|
up_boundi = max(starti, endi)
|
||||||
|
low_boundb = min(startb, endb)
|
||||||
|
low_boundi = min(starti, endi)
|
||||||
|
|
||||||
|
|
||||||
|
# In horizontal mode, X is the inline and Y is the block,
|
||||||
|
# but it's reversed for vertical modes, so reverse the lookup.
|
||||||
|
def at_point_vertical_mode(i, b):
|
||||||
|
return at_point(b, i)
|
||||||
|
|
||||||
|
at_point_conditional = at_point
|
||||||
|
if scroll_viewport.vertical_writing_mode:
|
||||||
|
at_point_conditional = at_point_vertical_mode
|
||||||
|
|
||||||
|
def i_loop(curb):
|
||||||
|
curi = starti
|
||||||
|
# The value will be either heading toward the lower or the upper bound,
|
||||||
|
# so just check for exceeding either bound
|
||||||
|
while low_boundi <= curi <= up_boundi:
|
||||||
|
cfi = at_point_conditional(curi, curb)
|
||||||
if cfi:
|
if cfi:
|
||||||
|
# print(f'found CFI at {curi}, {curb} {cfi}\n\t{starti} {deltai} {endi} {startb} {deltab} {endb}')
|
||||||
return cfi
|
return cfi
|
||||||
cury += delta
|
curi += deltai
|
||||||
|
|
||||||
|
curb = startb
|
||||||
|
while low_boundb <= curb <= up_boundb:
|
||||||
|
cfi = i_loop(curb)
|
||||||
|
if cfi:
|
||||||
|
break
|
||||||
|
curb += deltab
|
||||||
|
|
||||||
|
if cfi:
|
||||||
|
return cfi
|
||||||
|
|
||||||
# Use a spatial offset on the html element, since we could not find a
|
# Use a spatial offset on the html element, since we could not find a
|
||||||
# normal CFI
|
# normal CFI
|
||||||
x, y = window_scroll_pos()
|
x, y = scroll_viewport.x() + (0 if scroll_viewport.ltr else scroll_viewport.width()), scroll_viewport.y()
|
||||||
de = document.documentElement
|
de = document.documentElement
|
||||||
rect = de.getBoundingClientRect()
|
rect = de.getBoundingClientRect()
|
||||||
px = (x*100)/rect.width
|
px = (x*100)/rect.width
|
||||||
|
@ -2,15 +2,33 @@
|
|||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
from __python__ import bound_methods, hash_literals
|
from __python__ import bound_methods, hash_literals
|
||||||
|
|
||||||
|
# Notes about flow mode scrolling:
|
||||||
|
# All the math in flow 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, flow mode scrolls in the block direction.
|
||||||
|
#
|
||||||
|
# In vertical RTL books, the block position scrolls from right to left. |<------|
|
||||||
|
# This means that the viewport positions become negative as you scroll.
|
||||||
|
# This is hidden from flow 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 flow mode's math can safely pretend that
|
||||||
|
# things scroll in the positive block direction.
|
||||||
|
|
||||||
from dom import set_css
|
from dom import set_css
|
||||||
|
from read_book.cfi import scroll_to as cfi_scroll_to
|
||||||
from read_book.globals import current_spine_item, get_boss, rtl_page_progression, ltr_page_progression
|
from read_book.globals import current_spine_item, get_boss, rtl_page_progression, ltr_page_progression
|
||||||
from read_book.settings import opts
|
from read_book.settings import opts
|
||||||
from read_book.viewport import line_height, scroll_viewport
|
from read_book.viewport import line_height, rem_size, scroll_viewport
|
||||||
from utils import document_height
|
|
||||||
|
|
||||||
|
|
||||||
def flow_to_scroll_fraction(frac, on_initial_load):
|
def flow_to_scroll_fraction(frac, on_initial_load):
|
||||||
scroll_viewport.scroll_to(0, document_height() * frac)
|
scroll_viewport.scroll_to_in_block_direction(scroll_viewport.document_block_size() * frac)
|
||||||
|
|
||||||
|
|
||||||
small_scroll_events = v'[]'
|
small_scroll_events = v'[]'
|
||||||
@ -31,7 +49,7 @@ def dispatch_small_scrolls():
|
|||||||
for x in small_scroll_events:
|
for x in small_scroll_events:
|
||||||
amt += x.amt
|
amt += x.amt
|
||||||
clear_small_scrolls()
|
clear_small_scrolls()
|
||||||
get_boss().report_human_scroll(amt / document_height())
|
get_boss().report_human_scroll(amt / scroll_viewport.document_block_size())
|
||||||
|
|
||||||
|
|
||||||
def add_small_scroll(amt):
|
def add_small_scroll(amt):
|
||||||
@ -45,7 +63,7 @@ def report_human_scroll(amt):
|
|||||||
if amt > 0:
|
if amt > 0:
|
||||||
if is_large_scroll:
|
if is_large_scroll:
|
||||||
clear_small_scrolls()
|
clear_small_scrolls()
|
||||||
get_boss().report_human_scroll(amt / document_height())
|
get_boss().report_human_scroll(amt / scroll_viewport.document_block_size())
|
||||||
else:
|
else:
|
||||||
add_small_scroll(amt)
|
add_small_scroll(amt)
|
||||||
elif amt is 0 or is_large_scroll:
|
elif amt is 0 or is_large_scroll:
|
||||||
@ -56,13 +74,13 @@ last_change_spine_item_request = {}
|
|||||||
|
|
||||||
|
|
||||||
def _check_for_scroll_end(func, obj, args, report):
|
def _check_for_scroll_end(func, obj, args, report):
|
||||||
before = window.pageYOffset
|
before = scroll_viewport.block_pos()
|
||||||
should_flip_progression_direction = func.apply(obj, args)
|
should_flip_progression_direction = func.apply(obj, args)
|
||||||
|
|
||||||
now = window.performance.now()
|
now = window.performance.now()
|
||||||
scroll_animator.sync(now)
|
scroll_animator.sync(now)
|
||||||
|
|
||||||
if window.pageYOffset is before:
|
if scroll_viewport.block_pos() is before:
|
||||||
csi = current_spine_item()
|
csi = current_spine_item()
|
||||||
if last_change_spine_item_request.name is csi.name and now - last_change_spine_item_request.at < 2000:
|
if last_change_spine_item_request.name is csi.name and now - last_change_spine_item_request.at < 2000:
|
||||||
return False
|
return False
|
||||||
@ -74,7 +92,7 @@ def _check_for_scroll_end(func, obj, args, report):
|
|||||||
get_boss().send_message('next_spine_item', previous=go_to_previous_page)
|
get_boss().send_message('next_spine_item', previous=go_to_previous_page)
|
||||||
return False
|
return False
|
||||||
if report:
|
if report:
|
||||||
report_human_scroll(window.pageYOffset - before)
|
report_human_scroll(scroll_viewport.block_pos() - before)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -89,45 +107,63 @@ def check_for_scroll_end_and_report(func):
|
|||||||
|
|
||||||
|
|
||||||
@check_for_scroll_end_and_report
|
@check_for_scroll_end_and_report
|
||||||
def scroll_by(y):
|
def scroll_by_and_check_next_page(y):
|
||||||
window.scrollBy(0, y)
|
scroll_viewport.scroll_by_in_block_direction(y)
|
||||||
# This indicates to check_for_scroll_end_and_report that it should not
|
# This indicates to check_for_scroll_end_and_report that it should not
|
||||||
# flip the page progression direction.
|
# flip the page progression direction.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def flow_onwheel(evt):
|
def flow_onwheel(evt):
|
||||||
dx = dy = 0
|
di = db = 0
|
||||||
WheelEvent = window.WheelEvent
|
WheelEvent = window.WheelEvent
|
||||||
|
# Y deltas always scroll in the previous and next page direction,
|
||||||
|
# regardless of writing direction, since doing otherwise would
|
||||||
|
# make mouse wheels mostly useless for scrolling in books written
|
||||||
|
# vertically.
|
||||||
if evt.deltaY:
|
if evt.deltaY:
|
||||||
if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL:
|
if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL:
|
||||||
dy = evt.deltaY
|
db = evt.deltaY
|
||||||
elif evt.deltaMode is WheelEvent.DOM_DELTA_LINE:
|
elif evt.deltaMode is WheelEvent.DOM_DELTA_LINE:
|
||||||
dy = line_height() * evt.deltaY
|
db = line_height() * evt.deltaY
|
||||||
if evt.deltaMode is WheelEvent.DOM_DELTA_PAGE:
|
if evt.deltaMode is WheelEvent.DOM_DELTA_PAGE:
|
||||||
dy = (scroll_viewport.height() - 30) * evt.deltaY
|
db = (scroll_viewport.block_size() - 30) * evt.deltaY
|
||||||
|
# X deltas scroll horizontally in both horizontal and vertical books.
|
||||||
|
# It's more natural in both cases.
|
||||||
if evt.deltaX:
|
if evt.deltaX:
|
||||||
if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL:
|
if evt.deltaMode is WheelEvent.DOM_DELTA_PIXEL:
|
||||||
dx = evt.deltaX
|
dx = evt.deltaX
|
||||||
elif evt.deltaMode is WheelEvent.DOM_DELTA_LINE:
|
elif evt.deltaMode is WheelEvent.DOM_DELTA_LINE:
|
||||||
dx = 15 * evt.deltaX
|
dx = line_height() * evt.deltaX
|
||||||
else:
|
else:
|
||||||
dx = (scroll_viewport.width() - 30) * evt.deltaX
|
dx = (scroll_viewport.block_size() - 30) * evt.deltaX
|
||||||
if dx:
|
|
||||||
window.scrollBy(dx, 0)
|
if scroll_viewport.horizontal_writing_mode:
|
||||||
elif Math.abs(dy) >= 1:
|
di = dx
|
||||||
scroll_by(dy)
|
else:
|
||||||
|
# Left goes forward, so make sure left is positive and right is negative,
|
||||||
|
# which is the opposite of what the wheel sends.
|
||||||
|
if scroll_viewport.rtl:
|
||||||
|
db = -dx
|
||||||
|
# Right goes forward, so the sign is correct.
|
||||||
|
else:
|
||||||
|
db = dx
|
||||||
|
if di:
|
||||||
|
scroll_viewport.scroll_by_in_inline_direction(di)
|
||||||
|
elif Math.abs(db) >= 1:
|
||||||
|
scroll_by_and_check_next_page(db)
|
||||||
|
|
||||||
@check_for_scroll_end
|
@check_for_scroll_end
|
||||||
def goto_boundary(dir):
|
def goto_boundary(dir):
|
||||||
scroll_viewport.scroll_to(scroll_viewport.x(), 0 if dir is DIRECTION.Up else document_height())
|
position = 0 if dir is DIRECTION.Up else scroll_viewport.document_block_size()
|
||||||
|
scroll_viewport.scroll_to_in_block_direction(position)
|
||||||
get_boss().report_human_scroll()
|
get_boss().report_human_scroll()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@check_for_scroll_end_and_report
|
@check_for_scroll_end_and_report
|
||||||
def scroll_by_page(direction, flip_if_rtl_page_progression):
|
def scroll_by_page(direction, flip_if_rtl_page_progression):
|
||||||
h = scroll_viewport.height() - 10
|
b = scroll_viewport.block_size() - 10
|
||||||
window.scrollBy(0, h * direction)
|
scroll_viewport.scroll_by_in_block_direction(b * direction)
|
||||||
|
|
||||||
# Let check_for_scroll_end_and_report know whether or not it should flip
|
# Let check_for_scroll_end_and_report know whether or not it should flip
|
||||||
# the progression direction.
|
# the progression direction.
|
||||||
@ -179,10 +215,10 @@ def handle_shortcut(sc_name, evt):
|
|||||||
goto_boundary(DIRECTION.Down)
|
goto_boundary(DIRECTION.Down)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'left':
|
if sc_name is 'left':
|
||||||
window.scrollBy(-15 if ltr_page_progression() else 15, 0)
|
scroll_by_and_check_next_page(-15 if ltr_page_progression() else 15, 0)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'right':
|
if sc_name is 'right':
|
||||||
window.scrollBy(15 if ltr_page_progression() else -15, 0)
|
scroll_by_and_check_next_page(15 if ltr_page_progression() else -15, 0)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'start_of_book':
|
if sc_name is 'start_of_book':
|
||||||
get_boss().send_message('goto_doc_boundary', start=True)
|
get_boss().send_message('goto_doc_boundary', start=True)
|
||||||
@ -191,10 +227,10 @@ def handle_shortcut(sc_name, evt):
|
|||||||
get_boss().send_message('goto_doc_boundary', start=False)
|
get_boss().send_message('goto_doc_boundary', start=False)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'pageup':
|
if sc_name is 'pageup':
|
||||||
scroll_by_page(-1, flip_if_rtl_page_progression=False)
|
scroll_by_page(-1, False)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'pagedown':
|
if sc_name is 'pagedown':
|
||||||
scroll_by_page(1, flip_if_rtl_page_progression=False)
|
scroll_by_page(1, False)
|
||||||
return True
|
return True
|
||||||
if sc_name is 'toggle_autoscroll':
|
if sc_name is 'toggle_autoscroll':
|
||||||
toggle_autoscroll()
|
toggle_autoscroll()
|
||||||
@ -208,10 +244,16 @@ def handle_shortcut(sc_name, evt):
|
|||||||
|
|
||||||
def layout(is_single_page):
|
def layout(is_single_page):
|
||||||
line_height(True)
|
line_height(True)
|
||||||
|
rem_size(True)
|
||||||
set_css(document.body, margin='0', border_width='0', padding='0')
|
set_css(document.body, margin='0', border_width='0', padding='0')
|
||||||
# flow mode does not care about RTL vs LTR
|
body_style = window.getComputedStyle(document.body)
|
||||||
scroll_viewport.initialize_on_layout({'direction': 'ltr'})
|
# scroll viewport needs to know if we're in vertical mode,
|
||||||
|
# since that will cause scrolling to happen left and right
|
||||||
|
scroll_viewport.initialize_on_layout(body_style)
|
||||||
|
|
||||||
|
document.documentElement.style.overflow = 'hidden'
|
||||||
|
if scroll_viewport.vertical_writing_mode:
|
||||||
|
document.documentElement.style.overflow = 'visible'
|
||||||
|
|
||||||
def auto_scroll_resume():
|
def auto_scroll_resume():
|
||||||
scroll_animator.wait = False
|
scroll_animator.wait = False
|
||||||
@ -232,7 +274,7 @@ def cancel_scroll():
|
|||||||
|
|
||||||
|
|
||||||
def is_scroll_end(pos):
|
def is_scroll_end(pos):
|
||||||
return not (0 <= pos <= document_height() - window.innerHeight)
|
return not (0 <= pos <= scroll_viewport.document_block_size() - scroll_viewport.block_size())
|
||||||
|
|
||||||
|
|
||||||
DIRECTION = {'Up': -1, 'up': -1, 'Down': 1, 'down': 1, 'UP': -1, 'DOWN': 1}
|
DIRECTION = {'Up': -1, 'up': -1, 'Down': 1, 'down': 1, 'UP': -1, 'DOWN': 1}
|
||||||
@ -269,7 +311,7 @@ class ScrollAnimator:
|
|||||||
self.auto = auto
|
self.auto = auto
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.start_time = now
|
self.start_time = now
|
||||||
self.start_offset = window.pageYOffset
|
self.start_offset = scroll_viewport.block_pos()
|
||||||
self.csi_idx = current_spine_item().index
|
self.csi_idx = current_spine_item().index
|
||||||
self.animation_id = window.requestAnimationFrame(self.auto_scroll if auto else self.smooth_scroll)
|
self.animation_id = window.requestAnimationFrame(self.auto_scroll if auto else self.smooth_scroll)
|
||||||
|
|
||||||
@ -282,8 +324,8 @@ class ScrollAnimator:
|
|||||||
scroll_target = self.start_offset
|
scroll_target = self.start_offset
|
||||||
scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth) / 1000
|
scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth) / 1000
|
||||||
|
|
||||||
window.scrollTo(0, scroll_target)
|
scroll_viewport.scroll_to_in_block_direction(scroll_target)
|
||||||
amt = window.pageYOffset - self.start_offset
|
amt = scroll_viewport.block_pos() - self.start_offset
|
||||||
|
|
||||||
if is_scroll_end(scroll_target) and (not opts.scroll_stop_boundaries or (abs(amt) < 3 and duration is self.DURATION)):
|
if is_scroll_end(scroll_target) and (not opts.scroll_stop_boundaries or (abs(amt) < 3 and duration is self.DURATION)):
|
||||||
# "Turn the page" if stop at boundaries option is false or
|
# "Turn the page" if stop at boundaries option is false or
|
||||||
@ -305,7 +347,7 @@ class ScrollAnimator:
|
|||||||
scroll_target = self.start_offset
|
scroll_target = self.start_offset
|
||||||
scroll_target += Math.trunc(self.direction * elapsed * line_height() * opts.lines_per_sec_auto) / 1000
|
scroll_target += Math.trunc(self.direction * elapsed * line_height() * opts.lines_per_sec_auto) / 1000
|
||||||
|
|
||||||
window.scrollTo(0, scroll_target)
|
scroll_viewport.scroll_to_in_block_direction(scroll_target)
|
||||||
scroll_finished = is_scroll_end(scroll_target)
|
scroll_finished = is_scroll_end(scroll_target)
|
||||||
|
|
||||||
# report every second
|
# report every second
|
||||||
@ -324,7 +366,7 @@ class ScrollAnimator:
|
|||||||
get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up)
|
get_boss().send_message('next_spine_item', previous=self.direction is DIRECTION.Up)
|
||||||
|
|
||||||
def report(self):
|
def report(self):
|
||||||
amt = window.pageYOffset - self.start_offset
|
amt = scroll_viewport.block_pos() - self.start_offset
|
||||||
if abs(amt) > 0 and self.csi_idx is current_spine_item().index:
|
if abs(amt) > 0 and self.csi_idx is current_spine_item().index:
|
||||||
report_human_scroll(amt)
|
report_human_scroll(amt)
|
||||||
|
|
||||||
@ -333,7 +375,7 @@ class ScrollAnimator:
|
|||||||
self.report()
|
self.report()
|
||||||
self.csi_idx = current_spine_item().index
|
self.csi_idx = current_spine_item().index
|
||||||
self.start_time = ts or window.performance.now()
|
self.start_time = ts or window.performance.now()
|
||||||
self.start_offset = window.pageYOffset
|
self.start_offset = scroll_viewport.block_pos()
|
||||||
else:
|
else:
|
||||||
self.resume()
|
self.resume()
|
||||||
|
|
||||||
@ -440,7 +482,7 @@ class DragScroller:
|
|||||||
progress = max(0, min(1, (ts - self.start_time) / duration)) # max/min to account for jitter
|
progress = max(0, min(1, (ts - self.start_time) / duration)) # max/min to account for jitter
|
||||||
scroll_target = self.start_offset
|
scroll_target = self.start_offset
|
||||||
scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth * self.speed_factor) / 1000
|
scroll_target += Math.trunc(self.direction * progress * duration * line_height() * opts.lines_per_sec_smooth * self.speed_factor) / 1000
|
||||||
window.scrollTo(0, scroll_target)
|
scroll_viewport.scroll_to_in_block_direction(scroll_target)
|
||||||
|
|
||||||
if progress < 1:
|
if progress < 1:
|
||||||
self.animation_id = window.requestAnimationFrame(self.smooth_scroll)
|
self.animation_id = window.requestAnimationFrame(self.smooth_scroll)
|
||||||
@ -456,7 +498,7 @@ class DragScroller:
|
|||||||
self.direction = direction
|
self.direction = direction
|
||||||
self.speed_factor = speed_factor
|
self.speed_factor = speed_factor
|
||||||
self.start_time = now
|
self.start_time = now
|
||||||
self.start_offset = window.pageYOffset
|
self.start_offset = scroll_viewport.block_pos()
|
||||||
self.animation_id = window.requestAnimationFrame(self.smooth_scroll)
|
self.animation_id = window.requestAnimationFrame(self.smooth_scroll)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -487,15 +529,31 @@ def handle_gesture(gesture):
|
|||||||
delta = gesture.points[-2] - gesture.points[-1]
|
delta = gesture.points[-2] - gesture.points[-1]
|
||||||
if Math.abs(delta) >= 1:
|
if Math.abs(delta) >= 1:
|
||||||
if gesture.axis is 'vertical':
|
if gesture.axis is 'vertical':
|
||||||
scroll_by(delta)
|
# Vertical writing scrolls left and right,
|
||||||
|
# so doing a vertical flick shouldn't change pages.
|
||||||
|
if scroll_viewport.vertical_writing_mode:
|
||||||
|
scroll_viewport.scroll_by(delta, 0)
|
||||||
|
# However, it might change pages in horizontal writing
|
||||||
|
else:
|
||||||
|
scroll_by_and_check_next_page(delta)
|
||||||
else:
|
else:
|
||||||
window.scrollBy(delta, 0)
|
# A horizontal flick should check for new pages in
|
||||||
|
# vertical modes, since they flow left and right.
|
||||||
|
if scroll_viewport.vertical_writing_mode:
|
||||||
|
scroll_by_and_check_next_page(delta)
|
||||||
|
# In horizontal modes, just move by the delta.
|
||||||
|
else:
|
||||||
|
scroll_viewport.scroll_by(delta, 0)
|
||||||
if not gesture.active and not gesture.is_held:
|
if not gesture.active and not gesture.is_held:
|
||||||
flick_animator.start(gesture)
|
flick_animator.start(gesture)
|
||||||
elif gesture.type is 'prev-page':
|
elif gesture.type is 'prev-page':
|
||||||
scroll_by_page(-1, flip_if_rtl_page_progression=False)
|
# should flip = False - previous is previous whether RTL or LTR.
|
||||||
|
# flipping of this command is handled higher up
|
||||||
|
scroll_by_page(-1, False)
|
||||||
elif gesture.type is 'next-page':
|
elif gesture.type is 'next-page':
|
||||||
scroll_by_page(1, flip_if_rtl_page_progression=False)
|
# should flip = False - next is next whether RTL or LTR.
|
||||||
|
# flipping of this command is handled higher up
|
||||||
|
scroll_by_page(1, False)
|
||||||
|
|
||||||
|
|
||||||
anchor_funcs = {
|
anchor_funcs = {
|
||||||
@ -503,22 +561,28 @@ anchor_funcs = {
|
|||||||
if not elem:
|
if not elem:
|
||||||
return 0, 0
|
return 0, 0
|
||||||
br = elem.getBoundingClientRect()
|
br = elem.getBoundingClientRect()
|
||||||
# Elements start on the right side in RTL mode,
|
|
||||||
# so be sure to return that side if in RTL.
|
# Start of object in the scrolling direction
|
||||||
x, y = scroll_viewport.viewport_to_document(
|
return scroll_viewport.viewport_to_document_block(
|
||||||
br.left if scroll_viewport.ltr else br.right,
|
scroll_viewport.rect_block_start(br), elem.ownerDocument)
|
||||||
br.top, elem.ownerDocument)
|
|
||||||
return y, x
|
|
||||||
,
|
,
|
||||||
'visibility': def visibility(pos):
|
'visibility': def visibility(pos):
|
||||||
y, x = pos
|
x, y = pos
|
||||||
if y < window.pageYOffset:
|
|
||||||
|
if jstype(x) is 'number':
|
||||||
|
pos = x
|
||||||
|
if jstype(y) is 'number' and scroll_viewport.horizontal_writing_mode:
|
||||||
|
pos = y
|
||||||
|
|
||||||
|
# Have to negate X if in RTL for the math to be correct,
|
||||||
|
# as the value that the scroll viewport returns is negated
|
||||||
|
if scroll_viewport.vertical_writing_mode and scroll_viewport.rtl:
|
||||||
|
pos = -pos
|
||||||
|
|
||||||
|
if pos < scroll_viewport.block_pos():
|
||||||
return -1
|
return -1
|
||||||
if y < window.pageYOffset + scroll_viewport.height():
|
if pos <= scroll_viewport.block_pos() + scroll_viewport.block_size():
|
||||||
if x < window.pageXOffset:
|
return 0
|
||||||
return -1
|
|
||||||
if x < window.pageXOffset + scroll_viewport.width():
|
|
||||||
return 0
|
|
||||||
return 1
|
return 1
|
||||||
,
|
,
|
||||||
'cmp': def cmp(a, b):
|
'cmp': def cmp(a, b):
|
||||||
@ -551,3 +615,15 @@ def ensure_selection_visible():
|
|||||||
p.scrollIntoView()
|
p.scrollIntoView()
|
||||||
return
|
return
|
||||||
p = p.parentNode
|
p = p.parentNode
|
||||||
|
|
||||||
|
def jump_to_cfi(cfi):
|
||||||
|
# Jump to the position indicated by the specified conformal fragment
|
||||||
|
# indicator.
|
||||||
|
cfi_scroll_to(cfi, def(x, y):
|
||||||
|
# block is vertical if text is horizontal
|
||||||
|
if scroll_viewport.horizontal_writing_mode:
|
||||||
|
scroll_viewport.scroll_to_in_block_direction(y)
|
||||||
|
# block is horizontal if text is vertical
|
||||||
|
else:
|
||||||
|
scroll_viewport.scroll_to_in_block_direction(x)
|
||||||
|
)
|
||||||
|
@ -24,7 +24,8 @@ from read_book.flow_mode import (
|
|||||||
handle_shortcut as flow_handle_shortcut, layout as flow_layout,
|
handle_shortcut as flow_handle_shortcut, layout as flow_layout,
|
||||||
scroll_by_page as flow_scroll_by_page,
|
scroll_by_page as flow_scroll_by_page,
|
||||||
scroll_to_extend_annotation as flow_annotation_scroll,
|
scroll_to_extend_annotation as flow_annotation_scroll,
|
||||||
start_drag_scroll as start_drag_scroll_flow
|
start_drag_scroll as start_drag_scroll_flow,
|
||||||
|
jump_to_cfi as flow_jump_to_cfi
|
||||||
)
|
)
|
||||||
from read_book.footnotes import is_footnote_link
|
from read_book.footnotes import is_footnote_link
|
||||||
from read_book.globals import (
|
from read_book.globals import (
|
||||||
@ -237,7 +238,7 @@ class IframeBoss:
|
|||||||
self.handle_navigation_shortcut = flow_handle_shortcut
|
self.handle_navigation_shortcut = flow_handle_shortcut
|
||||||
self._handle_gesture = flow_handle_gesture
|
self._handle_gesture = flow_handle_gesture
|
||||||
self.to_scroll_fraction = flow_to_scroll_fraction
|
self.to_scroll_fraction = flow_to_scroll_fraction
|
||||||
self.jump_to_cfi = scroll_to_cfi
|
self.jump_to_cfi = flow_jump_to_cfi
|
||||||
self.anchor_funcs = flow_anchor_funcs
|
self.anchor_funcs = flow_anchor_funcs
|
||||||
self.auto_scroll_action = flow_auto_scroll_action
|
self.auto_scroll_action = flow_auto_scroll_action
|
||||||
self.scroll_to_extend_annotation = flow_annotation_scroll
|
self.scroll_to_extend_annotation = flow_annotation_scroll
|
||||||
@ -361,7 +362,7 @@ class IframeBoss:
|
|||||||
if self.last_cfi:
|
if self.last_cfi:
|
||||||
cfi = self.last_cfi[len('epubcfi(/'):-1].partition('/')[2]
|
cfi = self.last_cfi[len('epubcfi(/'):-1].partition('/')[2]
|
||||||
if cfi:
|
if cfi:
|
||||||
paged_jump_to_cfi('/' + cfi)
|
self.jump_to_cfi('/' + cfi)
|
||||||
self.update_cfi()
|
self.update_cfi()
|
||||||
self.update_toc_position()
|
self.update_toc_position()
|
||||||
|
|
||||||
@ -442,7 +443,6 @@ class IframeBoss:
|
|||||||
si = spine[i]
|
si = spine[i]
|
||||||
if si:
|
if si:
|
||||||
self.length_before += files[si]?.length or 0
|
self.length_before += files[si]?.length or 0
|
||||||
self.onscroll()
|
|
||||||
self.send_message('content_loaded', progress_frac=self.calculate_progress_frac(), file_progress_frac=progress_frac())
|
self.send_message('content_loaded', progress_frac=self.calculate_progress_frac(), file_progress_frac=progress_frac())
|
||||||
self.last_cfi = None
|
self.last_cfi = None
|
||||||
self.auto_scroll_action('resume')
|
self.auto_scroll_action('resume')
|
||||||
@ -547,7 +547,7 @@ class IframeBoss:
|
|||||||
if self.last_cfi:
|
if self.last_cfi:
|
||||||
cfi = self.last_cfi[len('epubcfi(/'):-1].partition('/')[2]
|
cfi = self.last_cfi[len('epubcfi(/'):-1].partition('/')[2]
|
||||||
if cfi:
|
if cfi:
|
||||||
paged_jump_to_cfi('/' + cfi)
|
self.jump_to_cfi('/' + cfi)
|
||||||
if current_layout_mode() is not 'flow':
|
if current_layout_mode() is not 'flow':
|
||||||
paged_resize_done()
|
paged_resize_done()
|
||||||
self.update_cfi()
|
self.update_cfi()
|
||||||
|
@ -1,5 +1,26 @@
|
|||||||
# vim:fileencoding=utf-8
|
# vim:fileencoding=utf-8
|
||||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
# 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
|
from __python__ import hash_literals
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
@ -13,9 +34,9 @@ from read_book.cfi import (
|
|||||||
)
|
)
|
||||||
from read_book.globals import current_spine_item, get_boss, rtl_page_progression
|
from read_book.globals import current_spine_item, get_boss, rtl_page_progression
|
||||||
from read_book.settings import opts
|
from read_book.settings import opts
|
||||||
from read_book.viewport import scroll_viewport, line_height
|
from read_book.viewport import scroll_viewport, line_height, rem_size
|
||||||
from utils import (
|
from utils import (
|
||||||
document_height, document_width, get_elem_data, set_elem_data
|
get_elem_data, set_elem_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -54,65 +75,76 @@ def in_paged_mode():
|
|||||||
return _in_paged_mode
|
return _in_paged_mode
|
||||||
|
|
||||||
|
|
||||||
col_width = screen_width = screen_height = cols_per_screen = gap = col_and_gap = number_of_cols = last_scrolled_to_column = 0
|
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
|
is_full_screen_layout = False
|
||||||
|
|
||||||
|
|
||||||
def reset_paged_mode_globals():
|
def reset_paged_mode_globals():
|
||||||
nonlocal _in_paged_mode, col_width, col_and_gap, screen_height, gap, screen_width, is_full_screen_layout, cols_per_screen, number_of_cols, last_scrolled_to_column
|
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()
|
scroll_viewport.reset_globals()
|
||||||
col_width = screen_width = screen_height = cols_per_screen = gap = col_and_gap = number_of_cols = last_scrolled_to_column = 0
|
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
|
is_full_screen_layout = _in_paged_mode = False
|
||||||
resize_manager.reset()
|
resize_manager.reset()
|
||||||
|
|
||||||
def column_at(xpos):
|
def column_at(pos):
|
||||||
# Return the (zero-based) number of the column that contains xpos
|
# Return the (zero-based) number of the column that contains pos
|
||||||
sw = scroll_viewport.paged_content_width()
|
si = scroll_viewport.paged_content_inline_size()
|
||||||
if xpos >= sw - col_and_gap:
|
if pos >= si - col_and_gap:
|
||||||
xpos = sw - col_width + 10
|
pos = si - col_size + 10
|
||||||
return (xpos + gap) // col_and_gap
|
return (pos + gap) // col_and_gap
|
||||||
|
|
||||||
def fit_images():
|
def fit_images():
|
||||||
# Ensure no images are wider than the available width in a column. Note
|
# Ensure no images are wider than the available size of a column. Note
|
||||||
# that this method use getBoundingClientRect() which means it will
|
# that this method use getBoundingClientRect() which means it will
|
||||||
# force a relayout if the render tree is dirty.
|
# force a relayout if the render tree is dirty.
|
||||||
images = v'[]'
|
inline_limited_images = v'[]'
|
||||||
vimages = v'[]'
|
block_limited_images = v'[]'
|
||||||
img_tags = document.getElementsByTagName('img')
|
img_tags = document.getElementsByTagName('img')
|
||||||
bounding_rects = v'[]'
|
bounding_rects = v'[]'
|
||||||
for img_tag in img_tags:
|
for img_tag in img_tags:
|
||||||
bounding_rects.push(img_tag.getBoundingClientRect())
|
bounding_rects.push(img_tag.getBoundingClientRect())
|
||||||
maxh = screen_height
|
maxb = screen_block
|
||||||
for i in range(img_tags.length):
|
for i in range(img_tags.length):
|
||||||
img = img_tags[i]
|
img = img_tags[i]
|
||||||
br = bounding_rects[i]
|
br = bounding_rects[i]
|
||||||
previously_limited = get_elem_data(img, 'width-limited', False)
|
previously_limited = get_elem_data(img, 'inline-limited', False)
|
||||||
data = get_elem_data(img, 'img-data', None)
|
data = get_elem_data(img, 'img-data', None)
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display}
|
data = {'left':br.left, 'right':br.right, 'height':br.height, 'display': img.style.display}
|
||||||
set_elem_data(img, 'img-data', data)
|
set_elem_data(img, 'img-data', data)
|
||||||
left = scroll_viewport.viewport_to_document(br.left, 0, img.ownerDocument)[0]
|
|
||||||
col = column_at(left) * col_and_gap
|
# Get start of image bounding box in the column direction (inline)
|
||||||
rleft = left - col
|
image_start = scroll_viewport.viewport_to_document_inline(scroll_viewport.rect_inline_start(br), img.ownerDocument)
|
||||||
width = br.right - br.left
|
col_start = column_at(image_start) * col_and_gap
|
||||||
rright = rleft + width
|
# Get inline distance from the start of the column to the start of the image bounding box
|
||||||
if previously_limited or rright > col_width:
|
column_start_to_image_start = image_start - col_start
|
||||||
images.push(v'[img, col_width - rleft]')
|
image_block_size = scroll_viewport.rect_block_size(br)
|
||||||
previously_limited = get_elem_data(img, 'height-limited', False)
|
image_inline_size = scroll_viewport.rect_inline_size(br)
|
||||||
if previously_limited or br.height > maxh or (br.height is maxh and br.width > col_width):
|
# Get the inline distance from the start of the column to the end of the image
|
||||||
vimages.push(img)
|
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:
|
if previously_limited:
|
||||||
set_css(img, break_before='auto', display=data.display)
|
set_css(img, break_before='auto', display=data.display)
|
||||||
set_css(img, break_inside='avoid')
|
set_css(img, break_inside='avoid')
|
||||||
|
|
||||||
for img_tag, max_width in images:
|
for img_tag, max_inline_size in inline_limited_images:
|
||||||
img_tag.style.setProperty('max-width', max_width+'px')
|
if scroll_viewport.vertical_writing_mode:
|
||||||
set_elem_data(img_tag, 'width-limited', True)
|
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 vimages:
|
for img_tag in block_limited_images:
|
||||||
data = get_elem_data(img_tag, 'img-data', None)
|
if scroll_viewport.vertical_writing_mode:
|
||||||
set_css(img_tag, break_before='always', max_height='100vh')
|
set_css(img_tag, break_before='always', max_width='100vw')
|
||||||
set_elem_data(img, 'height-limited', True)
|
else:
|
||||||
|
set_css(img_tag, break_before='always', max_height='100vh')
|
||||||
|
set_elem_data(img_tag, 'block-limited', True)
|
||||||
|
|
||||||
|
|
||||||
def cps_by_em_size():
|
def cps_by_em_size():
|
||||||
@ -143,7 +175,7 @@ def calc_columns_per_screen():
|
|||||||
except:
|
except:
|
||||||
cps = 0
|
cps = 0
|
||||||
if not cps:
|
if not cps:
|
||||||
cps = int(Math.floor(scroll_viewport.width() / (35 * cps_by_em_size())))
|
cps = int(Math.floor(scroll_viewport.inline_size() / (35 * cps_by_em_size())))
|
||||||
cps = max(1, min(cps or 1, 20))
|
cps = max(1, min(cps or 1, 20))
|
||||||
return cps
|
return cps
|
||||||
|
|
||||||
@ -157,13 +189,10 @@ def will_columns_per_screen_change():
|
|||||||
return calc_columns_per_screen() != cols_per_screen
|
return calc_columns_per_screen() != cols_per_screen
|
||||||
|
|
||||||
|
|
||||||
def current_page_width():
|
|
||||||
return col_width
|
|
||||||
|
|
||||||
|
|
||||||
def layout(is_single_page, on_resize):
|
def layout(is_single_page, on_resize):
|
||||||
nonlocal _in_paged_mode, col_width, col_and_gap, screen_height, gap, screen_width, is_full_screen_layout, cols_per_screen, number_of_cols
|
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)
|
line_height(True)
|
||||||
|
rem_size(True)
|
||||||
body_style = window.getComputedStyle(document.body)
|
body_style = window.getComputedStyle(document.body)
|
||||||
scroll_viewport.initialize_on_layout(body_style)
|
scroll_viewport.initialize_on_layout(body_style)
|
||||||
first_layout = not _in_paged_mode
|
first_layout = not _in_paged_mode
|
||||||
@ -172,7 +201,8 @@ def layout(is_single_page, on_resize):
|
|||||||
handle_rtl_body(body_style)
|
handle_rtl_body(body_style)
|
||||||
# Check if the current document is a full screen layout like
|
# Check if the current document is a full screen layout like
|
||||||
# cover, if so we treat it specially.
|
# cover, if so we treat it specially.
|
||||||
single_screen = (document_height() < scroll_viewport.height() + 75)
|
# (The inline size is the column direction, so it's the appropriate thing to check to make sure we don't have multiple columns)
|
||||||
|
single_screen = (scroll_viewport.document_inline_size() < scroll_viewport.inline_size() + 75)
|
||||||
first_layout = True
|
first_layout = True
|
||||||
svgs = document.getElementsByTagName('svg')
|
svgs = document.getElementsByTagName('svg')
|
||||||
has_svg = svgs.length > 0
|
has_svg = svgs.length > 0
|
||||||
@ -196,24 +226,24 @@ def layout(is_single_page, on_resize):
|
|||||||
create_page_div()
|
create_page_div()
|
||||||
|
|
||||||
n = cols_per_screen = cps
|
n = cols_per_screen = cps
|
||||||
# Calculate the column width so that cols_per_screen columns fit exactly in
|
# Calculate the column size so that cols_per_screen columns fit exactly in
|
||||||
# the window width, with their separator margins
|
# the window inline dimension, with their separator margins
|
||||||
ww = col_width = screen_width = scroll_viewport.width()
|
wi = col_size = screen_inline = scroll_viewport.inline_size()
|
||||||
sm = opts.margin_left + opts.margin_right
|
margin_size = opts.margin_left + opts.margin_right if scroll_viewport.horizontal_writing_mode else opts.margin_top + opts.margin_bottom
|
||||||
gap = sm
|
gap = margin_size
|
||||||
if n > 1:
|
if n > 1:
|
||||||
# Adjust the side margin so that the window width satisfies
|
# Adjust the margin so that the window inline dimension satisfies
|
||||||
# col_width * n + (n-1) * 2 * side_margin = window_width
|
# col_size * n + (n-1) * 2 * margin = window_inline
|
||||||
gap += ((ww + sm) % n) # Ensure ww + gap is a multiple of n
|
gap += ((wi + margin_size) % n) # Ensure wi + gap is a multiple of n
|
||||||
col_width = ((ww + gap) // n) - gap
|
col_size = ((wi + gap) // n) - gap
|
||||||
|
|
||||||
screen_height = scroll_viewport.height()
|
screen_block = scroll_viewport.block_size()
|
||||||
col_and_gap = col_width + gap
|
col_and_gap = col_size + gap
|
||||||
|
|
||||||
set_css(document.body, column_gap=gap + 'px', column_width=col_width + 'px', column_rule='0px inset blue',
|
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',
|
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',
|
margin='0', border_width='0', padding='0', box_sizing='content-box',
|
||||||
width=screen_width + 'px', height=screen_height + 'px', overflow_wrap='break-word'
|
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
|
# 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
|
# above the columns, which causes them to effectively be added to the
|
||||||
@ -240,67 +270,68 @@ def layout(is_single_page, on_resize):
|
|||||||
# with height=100% overflow the first column
|
# with height=100% overflow the first column
|
||||||
is_full_screen_layout = is_single_page
|
is_full_screen_layout = is_single_page
|
||||||
if not is_full_screen_layout:
|
if not is_full_screen_layout:
|
||||||
has_no_more_than_two_columns = (scroll_viewport.paged_content_width() < 2*ww + 10)
|
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 has_no_more_than_two_columns and single_screen:
|
||||||
if only_img and imgs.length and imgs[0].getBoundingClientRect().left < ww:
|
if only_img and imgs.length and imgs[0].getBoundingClientRect().left < wi:
|
||||||
is_full_screen_layout = True
|
is_full_screen_layout = True
|
||||||
if has_svg and svgs.length == 1 and svgs[0].getBoundingClientRect().left < ww:
|
if has_svg and svgs.length == 1 and svgs[0].getBoundingClientRect().left < wi:
|
||||||
is_full_screen_layout = True
|
is_full_screen_layout = True
|
||||||
|
|
||||||
# Some browser engine, WebKit at least, adjust column widths to please
|
# Some browser engine, WebKit at least, adjust column sizes to please
|
||||||
# themselves, unless the container width is an exact multiple, so we check
|
# themselves, unless the container size is an exact multiple, so we check
|
||||||
# for that and manually set the container widths.
|
# for that and manually set the container sizes.
|
||||||
def check_column_widths():
|
def check_column_sizes():
|
||||||
nonlocal number_of_cols
|
nonlocal number_of_cols
|
||||||
ncols = number_of_cols = (scroll_viewport.paged_content_width() + gap) / col_and_gap
|
ncols = number_of_cols = (scroll_viewport.paged_content_inline_size() + gap) / col_and_gap
|
||||||
if ncols is not Math.floor(ncols):
|
if ncols is not Math.floor(ncols):
|
||||||
n = number_of_cols = Math.floor(ncols)
|
n = number_of_cols = Math.floor(ncols)
|
||||||
dw = n*col_width + (n-1)*gap
|
dw = n*col_size + (n-1)*gap
|
||||||
data = {'col_width':col_width, 'gap':gap, 'scrollWidth':scroll_viewport.paged_content_width(), 'ncols':ncols, 'desired_width':dw}
|
data = {'col_size':col_size, 'gap':gap, 'scrollWidth':scroll_viewport.paged_content_inline_size(), 'ncols':ncols, 'desired_inline_size':dis}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
data = check_column_widths()
|
data = check_column_sizes()
|
||||||
if data:
|
if data:
|
||||||
dw = data.desired_width
|
dis = data.desired_inline_size
|
||||||
for elem in document.documentElement, document.body:
|
for elem in document.documentElement, document.body:
|
||||||
set_css(elem, max_width=dw + 'px', min_width=dw + 'px')
|
set_css(elem, max_width=dis + 'px', min_width=dis + 'px')
|
||||||
data = check_column_widths()
|
if scroll_viewport.vertical_writing_mode:
|
||||||
|
set_css(elem, max_height=dis + 'px', min_height=dis + 'px')
|
||||||
|
data = check_column_sizes()
|
||||||
if data:
|
if data:
|
||||||
print('WARNING: column layout broken, probably because there is some non-reflowable content in the book that is wider than the column width', 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
|
_in_paged_mode = True
|
||||||
fit_images()
|
fit_images()
|
||||||
return gap
|
return gap
|
||||||
|
|
||||||
def current_scroll_offset():
|
def current_scroll_offset():
|
||||||
return scroll_viewport.x()
|
return scroll_viewport.inline_pos()
|
||||||
|
|
||||||
|
|
||||||
def scroll_to_offset(x):
|
def scroll_to_offset(offset):
|
||||||
scroll_viewport.scroll_to(x, 0)
|
scroll_viewport.scroll_to_in_inline_direction(offset)
|
||||||
|
|
||||||
|
|
||||||
def scroll_to_column(number, notify=False, duration=1000):
|
def scroll_to_column(number, notify=False, duration=1000):
|
||||||
nonlocal last_scrolled_to_column
|
nonlocal last_scrolled_to_column
|
||||||
last_scrolled_to_column = number
|
last_scrolled_to_column = number
|
||||||
pos = number * col_and_gap
|
pos = number * col_and_gap
|
||||||
limit = scroll_viewport.paged_content_width() - screen_width
|
limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size()
|
||||||
pos = min(pos, limit)
|
pos = min(pos, limit)
|
||||||
scroll_to_offset(pos)
|
scroll_to_offset(pos)
|
||||||
|
|
||||||
|
|
||||||
def scroll_to_xpos(xpos, notify=False, duration=1000):
|
def scroll_to_pos(pos, notify=False, duration=1000):
|
||||||
nonlocal last_scrolled_to_column
|
nonlocal last_scrolled_to_column
|
||||||
# Scroll so that the column containing xpos is the left most column in
|
# Scroll to the column containing pos
|
||||||
# the viewport
|
if jstype(pos) is not 'number':
|
||||||
if jstype(xpos) is not 'number':
|
print(pos, 'is not a number, cannot scroll to it!')
|
||||||
print(xpos, 'is not a number, cannot scroll to it!')
|
|
||||||
return
|
return
|
||||||
if is_full_screen_layout:
|
if is_full_screen_layout:
|
||||||
scroll_to_offset(0)
|
scroll_to_offset(0)
|
||||||
last_scrolled_to_column = 0
|
last_scrolled_to_column = 0
|
||||||
return
|
return
|
||||||
scroll_to_column(column_at(xpos), notify=notify, duration=duration)
|
scroll_to_column(column_at(pos), notify=notify, duration=duration)
|
||||||
|
|
||||||
|
|
||||||
def scroll_to_previous_position():
|
def scroll_to_previous_position():
|
||||||
@ -315,8 +346,8 @@ def scroll_to_fraction(frac, on_initial_load):
|
|||||||
# Scroll to the position represented by frac (number between 0 and 1)
|
# 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():
|
if on_initial_load and frac is 1 and is_return() and scroll_to_previous_position():
|
||||||
return
|
return
|
||||||
xpos = Math.floor(scroll_viewport.paged_content_width() * frac)
|
pos = Math.floor(scroll_viewport.paged_content_inline_size() * frac)
|
||||||
scroll_to_xpos(xpos)
|
scroll_to_pos(pos)
|
||||||
|
|
||||||
|
|
||||||
def column_boundaries():
|
def column_boundaries():
|
||||||
@ -326,7 +357,7 @@ def column_boundaries():
|
|||||||
return l, l + cols_per_screen
|
return l, l + cols_per_screen
|
||||||
|
|
||||||
def current_column_location():
|
def current_column_location():
|
||||||
# The location of the left edge of the left most column currently
|
# The location of the starting edge of the first column currently
|
||||||
# visible in the viewport
|
# visible in the viewport
|
||||||
if is_full_screen_layout:
|
if is_full_screen_layout:
|
||||||
return 0
|
return 0
|
||||||
@ -346,10 +377,10 @@ def next_screen_location():
|
|||||||
if is_full_screen_layout:
|
if is_full_screen_layout:
|
||||||
return -1
|
return -1
|
||||||
cc = current_column_location()
|
cc = current_column_location()
|
||||||
ans = cc + screen_width
|
ans = cc + screen_inline
|
||||||
if cols_per_screen > 1 and 0 < number_of_cols_left() < cols_per_screen:
|
if cols_per_screen > 1 and 0 < number_of_cols_left() < cols_per_screen:
|
||||||
return -1 # Only blank, dummy pages left
|
return -1 # Only blank, dummy pages left
|
||||||
limit = scroll_viewport.paged_content_width() - scroll_viewport.width()
|
limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size()
|
||||||
if limit < col_and_gap:
|
if limit < col_and_gap:
|
||||||
return -1
|
return -1
|
||||||
if ans > limit:
|
if ans > limit:
|
||||||
@ -363,7 +394,7 @@ def previous_screen_location():
|
|||||||
if is_full_screen_layout:
|
if is_full_screen_layout:
|
||||||
return -1
|
return -1
|
||||||
cc = current_column_location()
|
cc = current_column_location()
|
||||||
ans = cc - screen_width
|
ans = cc - screen_inline
|
||||||
if ans < 0:
|
if ans < 0:
|
||||||
# We ignore small scrolls (less than 15px) when going to previous
|
# We ignore small scrolls (less than 15px) when going to previous
|
||||||
# screen
|
# screen
|
||||||
@ -379,8 +410,8 @@ def next_col_location():
|
|||||||
return -1
|
return -1
|
||||||
cc = current_column_location()
|
cc = current_column_location()
|
||||||
ans = cc + col_and_gap
|
ans = cc + col_and_gap
|
||||||
limit = scroll_viewport.paged_content_width() - scroll_viewport.width()
|
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_width={scroll_viewport.paged_content_width()} vw={scroll_viewport.width()} current_scroll_offset={current_scroll_offset()}')
|
# 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 ans > limit:
|
||||||
ans = limit if Math.ceil(current_scroll_offset()) < limit else -1
|
ans = limit if Math.ceil(current_scroll_offset()) < limit else -1
|
||||||
return ans
|
return ans
|
||||||
@ -400,9 +431,7 @@ def previous_col_location():
|
|||||||
|
|
||||||
|
|
||||||
def jump_to_anchor(name):
|
def jump_to_anchor(name):
|
||||||
# Jump to the element identified by anchor name. Ensures that the left
|
# Jump to the element identified by anchor name.
|
||||||
# most column in the viewport is the column containing the start of the
|
|
||||||
# element and that the scroll position is at the start of the column.
|
|
||||||
elem = document.getElementById(name)
|
elem = document.getElementById(name)
|
||||||
if not elem:
|
if not elem:
|
||||||
elems = document.getElementsByName(name)
|
elems = document.getElementsByName(name)
|
||||||
@ -429,13 +458,19 @@ def scroll_to_elem(elem):
|
|||||||
# elem.scrollIntoView(). However, in some cases it gives
|
# elem.scrollIntoView(). However, in some cases it gives
|
||||||
# inaccurate results, so we prefer the bounding client rect,
|
# inaccurate results, so we prefer the bounding client rect,
|
||||||
# when possible.
|
# when possible.
|
||||||
# Columns start on the right side in RTL mode, so get that instead here...
|
|
||||||
pos = elem.scrollLeft if scroll_viewport.ltr else elem.scrollRight
|
# 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:
|
else:
|
||||||
# and here.
|
# If we can use the rect, just use the simpler viewport helper function
|
||||||
pos = br.left if scroll_viewport.ltr else br.right
|
inline_start = scroll_viewport.rect_inline_start(br)
|
||||||
scroll_to_xpos(scroll_viewport.viewport_to_document(
|
|
||||||
pos+2, elem.scrollTop, elem.ownerDocument)[0])
|
scroll_to_pos(scroll_viewport.viewport_to_document_inline(inline_start+2, elem.ownerDocument))
|
||||||
|
|
||||||
def snap_to_selection():
|
def snap_to_selection():
|
||||||
# Ensure that the viewport is positioned at the start of the column
|
# Ensure that the viewport is positioned at the start of the column
|
||||||
@ -444,25 +479,22 @@ def snap_to_selection():
|
|||||||
sel = window.getSelection()
|
sel = window.getSelection()
|
||||||
r = sel.getRangeAt(0).getBoundingClientRect()
|
r = sel.getRangeAt(0).getBoundingClientRect()
|
||||||
node = sel.anchorNode
|
node = sel.anchorNode
|
||||||
# In RTL mode, the "start" of selection is on the right side.
|
# Columns are in the inline direction, so get the beginning of the element in the inline
|
||||||
pos = scroll_viewport.viewport_to_document(
|
pos = scroll_viewport.viewport_to_document_inline(
|
||||||
r.left if scroll_viewport.ltr else r.right,
|
scroll_viewport.rect_inline_start(r), doc=node.ownerDocument)
|
||||||
r.top, doc=node.ownerDocument)[0]
|
|
||||||
|
|
||||||
# Ensure we are scrolled to the column containing the start of the
|
# Ensure we are scrolled to the column containing the start of the
|
||||||
# selection
|
# selection
|
||||||
scroll_to_xpos(pos+5)
|
scroll_to_pos(pos+5)
|
||||||
|
|
||||||
def jump_to_cfi(cfi):
|
def jump_to_cfi(cfi):
|
||||||
# Jump to the position indicated by the specified conformal fragment
|
# Jump to the position indicated by the specified conformal fragment
|
||||||
# indicator. When in paged mode, the
|
# indicator.
|
||||||
# scroll is performed so that the column containing the position
|
|
||||||
# pointed to by the cfi is the left most column in the viewport
|
|
||||||
cfi_scroll_to(cfi, def(x, y):
|
cfi_scroll_to(cfi, def(x, y):
|
||||||
if in_paged_mode():
|
if scroll_viewport.horizontal_writing_mode:
|
||||||
scroll_to_xpos(x)
|
scroll_to_pos(x)
|
||||||
else:
|
else:
|
||||||
scroll_viewport.scroll_to(0, y)
|
scroll_to_pos(y)
|
||||||
)
|
)
|
||||||
|
|
||||||
def current_cfi():
|
def current_cfi():
|
||||||
@ -472,7 +504,7 @@ def current_cfi():
|
|||||||
if in_paged_mode():
|
if in_paged_mode():
|
||||||
for cnum in range(cols_per_screen):
|
for cnum in range(cols_per_screen):
|
||||||
left = cnum * (col_and_gap + gap)
|
left = cnum * (col_and_gap + gap)
|
||||||
right = left + col_width
|
right = left + col_size
|
||||||
top, bottom = 0, scroll_viewport.height()
|
top, bottom = 0, scroll_viewport.height()
|
||||||
midx = (right - left) // 2
|
midx = (right - left) // 2
|
||||||
deltax = (right - left) // 24
|
deltax = (right - left) // 24
|
||||||
@ -510,14 +542,15 @@ def current_cfi():
|
|||||||
def progress_frac(frac):
|
def progress_frac(frac):
|
||||||
# The current scroll position as a fraction between 0 and 1
|
# The current scroll position as a fraction between 0 and 1
|
||||||
if in_paged_mode():
|
if in_paged_mode():
|
||||||
limit = scroll_viewport.paged_content_width() - scroll_viewport.width()
|
limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size()
|
||||||
if limit <= 0:
|
if limit <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
return current_scroll_offset() / limit
|
return current_scroll_offset() / limit
|
||||||
limit = document_height() - scroll_viewport.height()
|
# 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:
|
if limit <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
return Math.max(0, Math.min(window.pageYOffset / limit, 1))
|
return Math.max(0, Math.min(scroll_viewport.block_pos() / limit, 1))
|
||||||
|
|
||||||
|
|
||||||
def next_spine_item(backward):
|
def next_spine_item(backward):
|
||||||
@ -570,13 +603,13 @@ class HandleWheel:
|
|||||||
def do_scroll(self, backward):
|
def do_scroll(self, backward):
|
||||||
self.reset()
|
self.reset()
|
||||||
if opts.paged_wheel_scrolls_by_screen:
|
if opts.paged_wheel_scrolls_by_screen:
|
||||||
x = previous_screen_location() if backward else next_screen_location()
|
pos = previous_screen_location() if backward else next_screen_location()
|
||||||
else:
|
else:
|
||||||
x = previous_col_location() if backward else next_col_location()
|
pos = previous_col_location() if backward else next_col_location()
|
||||||
if x is -1:
|
if pos is -1:
|
||||||
next_spine_item(backward)
|
next_spine_item(backward)
|
||||||
else:
|
else:
|
||||||
scroll_to_xpos(x)
|
scroll_to_pos(pos)
|
||||||
|
|
||||||
|
|
||||||
wheel_handler = HandleWheel()
|
wheel_handler = HandleWheel()
|
||||||
@ -603,14 +636,14 @@ def scroll_by_page(backward, by_screen, flip_if_rtl_page_progression):
|
|||||||
get_boss().report_human_scroll(scrolled_frac)
|
get_boss().report_human_scroll(scrolled_frac)
|
||||||
else:
|
else:
|
||||||
get_boss().report_human_scroll()
|
get_boss().report_human_scroll()
|
||||||
scroll_to_xpos(pos)
|
scroll_to_pos(pos)
|
||||||
|
|
||||||
|
|
||||||
def scroll_to_extend_annotation(backward):
|
def scroll_to_extend_annotation(backward):
|
||||||
pos = previous_col_location() if backward else next_col_location()
|
pos = previous_col_location() if backward else next_col_location()
|
||||||
if pos is -1:
|
if pos is -1:
|
||||||
return False
|
return False
|
||||||
scroll_to_xpos(pos)
|
scroll_to_pos(pos)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -627,7 +660,7 @@ def handle_shortcut(sc_name, evt):
|
|||||||
return True
|
return True
|
||||||
if sc_name is 'end_of_file':
|
if sc_name is 'end_of_file':
|
||||||
get_boss().report_human_scroll()
|
get_boss().report_human_scroll()
|
||||||
scroll_to_offset(document_width())
|
scroll_to_offset(scroll_viewport.document_inline_size())
|
||||||
return True
|
return True
|
||||||
if sc_name is 'left':
|
if sc_name is 'left':
|
||||||
scroll_by_page(backward=True, by_screen=False, flip_if_rtl_page_progression=True)
|
scroll_by_page(backward=True, by_screen=False, flip_if_rtl_page_progression=True)
|
||||||
@ -676,11 +709,9 @@ anchor_funcs = {
|
|||||||
if not elem:
|
if not elem:
|
||||||
return 0
|
return 0
|
||||||
br = elem.getBoundingClientRect()
|
br = elem.getBoundingClientRect()
|
||||||
# In RTL mode, the start of something is on the right side.
|
pos = scroll_viewport.viewport_to_document_inline(
|
||||||
x = scroll_viewport.viewport_to_document(
|
scroll_viewport.rect_inline_start(br))
|
||||||
br.left if scroll_viewport.ltr else br.right,
|
return column_at(pos)
|
||||||
br.top, elem.ownerDocument)[0]
|
|
||||||
return column_at(x)
|
|
||||||
,
|
,
|
||||||
'visibility': def visibility(pos):
|
'visibility': def visibility(pos):
|
||||||
first = column_at(current_scroll_offset() + 10)
|
first = column_at(current_scroll_offset() + 10)
|
||||||
@ -716,7 +747,7 @@ class ResizeManager:
|
|||||||
'width': scroll_viewport.width(), 'height': scroll_viewport.height(), 'column': last_scrolled_to_column}}
|
'width': scroll_viewport.width(), 'height': scroll_viewport.height(), 'column': last_scrolled_to_column}}
|
||||||
if self.is_inverse_transition(transition):
|
if self.is_inverse_transition(transition):
|
||||||
if transition.after.column is not self.last_transition.before.column:
|
if transition.after.column is not self.last_transition.before.column:
|
||||||
self.scroll_to_column(transition.after.column)
|
scroll_to_column(transition.after.column)
|
||||||
transition.after.column = last_scrolled_to_column
|
transition.after.column = last_scrolled_to_column
|
||||||
self.last_transition = transition
|
self.last_transition = transition
|
||||||
|
|
||||||
@ -766,7 +797,7 @@ class DragScroller:
|
|||||||
def do_one_page_turn(self):
|
def do_one_page_turn(self):
|
||||||
pos = previous_col_location() if self.backward else next_col_location()
|
pos = previous_col_location() if self.backward else next_col_location()
|
||||||
if pos >= 0:
|
if pos >= 0:
|
||||||
scroll_to_xpos(pos)
|
scroll_to_pos(pos)
|
||||||
self.timer_id = window.setTimeout(self.do_one_page_turn.bind(self), self.INTERVAL * 2)
|
self.timer_id = window.setTimeout(self.do_one_page_turn.bind(self), self.INTERVAL * 2)
|
||||||
else:
|
else:
|
||||||
self.stop()
|
self.stop()
|
||||||
|
@ -5,7 +5,7 @@ from __python__ import bound_methods, hash_literals
|
|||||||
FUNCTIONS = 'x y scroll_to scroll_into_view reset_globals __reset_transforms'.split(' ')
|
FUNCTIONS = 'x y scroll_to scroll_into_view reset_globals __reset_transforms'.split(' ')
|
||||||
|
|
||||||
from read_book.globals import get_boss, viewport_mode_changer
|
from read_book.globals import get_boss, viewport_mode_changer
|
||||||
from utils import is_ios
|
from utils import document_height, document_width, is_ios
|
||||||
|
|
||||||
class ScrollViewport:
|
class ScrollViewport:
|
||||||
|
|
||||||
@ -17,6 +17,8 @@ class ScrollViewport:
|
|||||||
# code into thinking that it's always scrolling in positive X.
|
# code into thinking that it's always scrolling in positive X.
|
||||||
self.rtl = False
|
self.rtl = False
|
||||||
self.ltr = True
|
self.ltr = True
|
||||||
|
self.vertical_writing_mode = False
|
||||||
|
self.horizontal_writing_mode = True
|
||||||
|
|
||||||
def set_mode(self, mode):
|
def set_mode(self, mode):
|
||||||
prefix = ('flow' if mode is 'flow' else 'paged') + '_'
|
prefix = ('flow' if mode is 'flow' else 'paged') + '_'
|
||||||
@ -24,12 +26,27 @@ class ScrollViewport:
|
|||||||
self[attr] = self[prefix + attr]
|
self[attr] = self[prefix + attr]
|
||||||
|
|
||||||
def initialize_on_layout(self, body_style):
|
def initialize_on_layout(self, body_style):
|
||||||
|
self.horizontal_writing_mode = True
|
||||||
|
self.vertical_writing_mode = False
|
||||||
|
self.ltr = True
|
||||||
|
self.rtl = False
|
||||||
if body_style.direction is "rtl":
|
if body_style.direction is "rtl":
|
||||||
self.rtl = True
|
self.rtl = True
|
||||||
self.ltr = False
|
self.ltr = False
|
||||||
|
|
||||||
|
css_vertical_rl = body_style.getPropertyValue("writing-mode") is "vertical-rl"
|
||||||
|
if css_vertical_rl:
|
||||||
|
self.vertical_writing_mode = True
|
||||||
|
self.horizontal_writing_mode = False
|
||||||
|
self.rtl = True
|
||||||
|
self.ltr = False
|
||||||
else:
|
else:
|
||||||
self.rtl = False
|
css_vertical_lr = body_style.getPropertyValue("writing-mode") is "vertical-lr"
|
||||||
self.ltr = True
|
if css_vertical_lr:
|
||||||
|
self.vertical_writing_mode = True
|
||||||
|
self.horizontal_writing_mode = False
|
||||||
|
self.ltr = True
|
||||||
|
self.rtl = False
|
||||||
|
|
||||||
def flow_x(self):
|
def flow_x(self):
|
||||||
if self.rtl:
|
if self.rtl:
|
||||||
@ -39,8 +56,15 @@ class ScrollViewport:
|
|||||||
def flow_y(self):
|
def flow_y(self):
|
||||||
return window.pageYOffset
|
return window.pageYOffset
|
||||||
|
|
||||||
def paged_y(self):
|
def inline_pos(self):
|
||||||
return 0
|
if self.vertical_writing_mode:
|
||||||
|
return self.y()
|
||||||
|
return self.x()
|
||||||
|
|
||||||
|
def block_pos(self):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return self.y()
|
||||||
|
return self.x()
|
||||||
|
|
||||||
def flow_scroll_to(self, x, y):
|
def flow_scroll_to(self, x, y):
|
||||||
if self.rtl:
|
if self.rtl:
|
||||||
@ -48,18 +72,71 @@ class ScrollViewport:
|
|||||||
else:
|
else:
|
||||||
window.scrollTo(x, y)
|
window.scrollTo(x, y)
|
||||||
|
|
||||||
|
def scroll_to_in_inline_direction(self, pos):
|
||||||
|
# Lines flow vertically, so inline is vertical.
|
||||||
|
if self.vertical_writing_mode:
|
||||||
|
self.scroll_to(0, pos)
|
||||||
|
else:
|
||||||
|
self.scroll_to(pos, 0)
|
||||||
|
|
||||||
|
def scroll_to_in_block_direction(self, pos):
|
||||||
|
# In horizontal modes, the block direction is vertical.
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
self.scroll_to(0, pos)
|
||||||
|
# In vertical modes, the block direction is horizontal.
|
||||||
|
else:
|
||||||
|
self.scroll_to(pos, 0)
|
||||||
|
|
||||||
def flow_scroll_into_view(self, elem):
|
def flow_scroll_into_view(self, elem):
|
||||||
elem.scrollIntoView()
|
elem.scrollIntoView()
|
||||||
|
|
||||||
|
def scroll_by(self, x, y):
|
||||||
|
if self.ltr:
|
||||||
|
window.scrollBy(x, y)
|
||||||
|
# Swap inline direction if in RTL mode.
|
||||||
|
else:
|
||||||
|
window.scrollBy(-x,y)
|
||||||
|
|
||||||
|
def scroll_by_in_inline_direction(self, offset):
|
||||||
|
# Same logic as scroll_to_in_inline_direction
|
||||||
|
if self.vertical_writing_mode:
|
||||||
|
self.scroll_by(0, offset)
|
||||||
|
else:
|
||||||
|
self.scroll_by(offset, 0)
|
||||||
|
|
||||||
|
def scroll_by_in_block_direction(self, offset):
|
||||||
|
# Same logic as scroll_to_in_block_direction
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
self.scroll_by(0, offset)
|
||||||
|
else:
|
||||||
|
self.scroll_by(offset, 0)
|
||||||
|
|
||||||
def flow_reset_globals(self):
|
def flow_reset_globals(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def flow___reset_transforms(self):
|
def flow___reset_transforms(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def paged_content_width(self):
|
def paged_content_inline_size(self):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return document.documentElement.scrollWidth
|
||||||
|
return document.documentElement.scrollHeight
|
||||||
|
|
||||||
|
def paged_content_block_size(self):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return document.documentElement.scrollHeight
|
||||||
return document.documentElement.scrollWidth
|
return document.documentElement.scrollWidth
|
||||||
|
|
||||||
|
def inline_size(self):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return self.width()
|
||||||
|
return self.height()
|
||||||
|
|
||||||
|
def block_size(self):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return self.height()
|
||||||
|
return self.width()
|
||||||
|
|
||||||
def update_window_size(self, w, h):
|
def update_window_size(self, w, h):
|
||||||
self.window_width_from_parent = w
|
self.window_width_from_parent = w
|
||||||
self.window_height_from_parent = h
|
self.window_height_from_parent = h
|
||||||
@ -70,6 +147,16 @@ class ScrollViewport:
|
|||||||
def height(self):
|
def height(self):
|
||||||
return window.innerHeight
|
return window.innerHeight
|
||||||
|
|
||||||
|
def document_inline_size(self):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return document_width()
|
||||||
|
return document_height()
|
||||||
|
|
||||||
|
def document_block_size(self):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return document_height()
|
||||||
|
return document_width()
|
||||||
|
|
||||||
# Assure that the viewport position returned is corrected for the RTL
|
# Assure that the viewport position returned is corrected for the RTL
|
||||||
# mode of ScrollViewport.
|
# mode of ScrollViewport.
|
||||||
def viewport_to_document(self, x, y, doc):
|
def viewport_to_document(self, x, y, doc):
|
||||||
@ -94,6 +181,72 @@ class ScrollViewport:
|
|||||||
return -x, y
|
return -x, y
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
|
def rect_inline_start(self, rect):
|
||||||
|
# Lines start on the left in LTR mode, right in RTL mode
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return rect.left if self.ltr else rect.right
|
||||||
|
# Only top-to-bottom vertical writing is supported
|
||||||
|
return rect.top
|
||||||
|
|
||||||
|
def rect_inline_end(self, rect):
|
||||||
|
# Lines end on the right in LTR mode, left in RTL mode
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return rect.right if self.ltr else rect.left
|
||||||
|
# In top-to-bottom (the only vertical mode supported), bottom:
|
||||||
|
return rect.bottom
|
||||||
|
|
||||||
|
def rect_block_start(self, rect):
|
||||||
|
# Block flows top to bottom in horizontal modes
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return rect.top
|
||||||
|
# Block flows either left or right in vertical modes
|
||||||
|
return rect.left if self.ltr else rect.right
|
||||||
|
|
||||||
|
def rect_block_end(self, rect):
|
||||||
|
# Block flows top to bottom in horizontal modes
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return rect.bottom
|
||||||
|
# Block flows either left or right in vertical modes
|
||||||
|
return rect.right if self.ltr else rect.left
|
||||||
|
|
||||||
|
def rect_inline_size(self, rect):
|
||||||
|
# Lines go horizontally in horizontal writing, so use width
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return rect.width
|
||||||
|
return rect.height
|
||||||
|
|
||||||
|
def rect_block_size(self, rect):
|
||||||
|
# The block is vertical in horizontal writing, so use height
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return rect.height
|
||||||
|
return rect.width
|
||||||
|
|
||||||
|
# Returns document inline coordinate (1 value) at viewport inline coordinate
|
||||||
|
def viewport_to_document_inline(self, pos, doc):
|
||||||
|
# Lines flow horizontally in horizontal mode
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return self.viewport_to_document(pos, 0, doc)[0]
|
||||||
|
# Inline is vertical in vertical mode
|
||||||
|
return self.viewport_to_document(0, pos, doc)[1]
|
||||||
|
|
||||||
|
# Returns document block coordinate (1 value) at viewport block coordinate
|
||||||
|
def viewport_to_document_block(self, pos, doc):
|
||||||
|
# Block is vertical in horizontal mode
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return self.viewport_to_document(0, pos, doc)[1]
|
||||||
|
# Horizontal in vertical mode
|
||||||
|
return self.viewport_to_document(pos, 0, doc)[0]
|
||||||
|
|
||||||
|
def viewport_to_document_inline_block(self, inline, block):
|
||||||
|
if self.horizontal_writing_mode:
|
||||||
|
return self.viewport_to_document(inline, block)
|
||||||
|
return self.viewport_to_document(block, inline)
|
||||||
|
|
||||||
|
def element_from_point(self, doc, x, y):
|
||||||
|
if self.rtl:
|
||||||
|
return doc.elementFromPoint(-x, y)
|
||||||
|
return doc.elementFromPoint(x, y)
|
||||||
|
|
||||||
class IOSScrollViewport(ScrollViewport):
|
class IOSScrollViewport(ScrollViewport):
|
||||||
|
|
||||||
def width(self):
|
def width(self):
|
||||||
@ -102,15 +255,19 @@ class IOSScrollViewport(ScrollViewport):
|
|||||||
def height(self):
|
def height(self):
|
||||||
return self.window_height_from_parent or window.innerHeight
|
return self.window_height_from_parent or window.innerHeight
|
||||||
|
|
||||||
def _scroll_implementation(self, x):
|
def _scroll_implementation(self, x, y):
|
||||||
if x is 0:
|
if x is 0 and y is 0:
|
||||||
document.documentElement.style.transform = 'none'
|
document.documentElement.style.transform = 'none'
|
||||||
else:
|
else:
|
||||||
x *= -1
|
x *= -1
|
||||||
document.documentElement.style.transform = f'translateX({x}px)'
|
y *= -1
|
||||||
|
document.documentElement.style.transform = f'translateX({x}px) translateY({y}px)'
|
||||||
|
|
||||||
def paged_scroll_to(self, x, y):
|
def paged_scroll_to(self, x, y):
|
||||||
self._scroll_implementation(x)
|
if self.ltr:
|
||||||
|
self._scroll_implementation(x, y)
|
||||||
|
else:
|
||||||
|
self._scroll_implementation(-x, y)
|
||||||
boss = get_boss()
|
boss = get_boss()
|
||||||
if boss:
|
if boss:
|
||||||
boss.onscroll()
|
boss.onscroll()
|
||||||
@ -127,6 +284,17 @@ class IOSScrollViewport(ScrollViewport):
|
|||||||
ans *= -1
|
ans *= -1
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def paged_y(self):
|
||||||
|
raw = document.documentElement.style.transform
|
||||||
|
if not raw or raw is 'none':
|
||||||
|
return 0
|
||||||
|
raw = raw[raw.lastIndexOf('(') + 1:]
|
||||||
|
ans = parseInt(raw)
|
||||||
|
if isNaN(ans):
|
||||||
|
return 0
|
||||||
|
ans *= -1
|
||||||
|
return ans
|
||||||
|
|
||||||
def paged_scroll_into_view(self, elem):
|
def paged_scroll_into_view(self, elem):
|
||||||
left = elem.offsetLeft
|
left = elem.offsetLeft
|
||||||
if left is None:
|
if left is None:
|
||||||
@ -158,6 +326,23 @@ for attr in FUNCTIONS:
|
|||||||
scroll_viewport['paged_' + attr] = scroll_viewport[attr]
|
scroll_viewport['paged_' + attr] = scroll_viewport[attr]
|
||||||
viewport_mode_changer(scroll_viewport.set_mode)
|
viewport_mode_changer(scroll_viewport.set_mode)
|
||||||
|
|
||||||
|
def rem_size(reset):
|
||||||
|
if reset:
|
||||||
|
rem_size.ans = None
|
||||||
|
return
|
||||||
|
if not rem_size.ans:
|
||||||
|
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)
|
||||||
|
rem_size.ans = d.clientWidth
|
||||||
|
document.body.removeChild(d)
|
||||||
|
return rem_size.ans
|
||||||
|
|
||||||
def line_height(reset):
|
def line_height(reset):
|
||||||
if reset:
|
if reset:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user