mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-04-04 16:21:57 -04:00
383 lines
12 KiB
Plaintext
383 lines
12 KiB
Plaintext
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
|
from __python__ import bound_methods, hash_literals
|
|
|
|
FUNCTIONS = 'x y scroll_to scroll_into_view reset_globals __reset_transforms window_scroll_pos content_size'.split(' ')
|
|
|
|
from read_book.globals import get_boss, viewport_mode_changer
|
|
from utils import document_height, document_width, ios_major_version
|
|
|
|
class ScrollViewport:
|
|
|
|
def __init__(self):
|
|
self.set_mode('flow')
|
|
self.window_width_from_parent = self.window_height_from_parent = None
|
|
# In RTL mode, we hide the fact that we are scrolling to the left by negating the
|
|
# current X position and the requested X scroll position, which fools the reader
|
|
# code into thinking that it's always scrolling in positive X.
|
|
self.rtl = False
|
|
self.ltr = True
|
|
self.vertical_writing_mode = False
|
|
self.horizontal_writing_mode = True
|
|
|
|
def set_mode(self, mode):
|
|
prefix = ('flow' if mode is 'flow' else 'paged') + '_'
|
|
for attr in FUNCTIONS:
|
|
self[attr] = self[prefix + attr]
|
|
|
|
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":
|
|
self.rtl = True
|
|
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:
|
|
css_vertical_lr = body_style.getPropertyValue("writing-mode") is "vertical-lr"
|
|
if css_vertical_lr:
|
|
self.vertical_writing_mode = True
|
|
self.horizontal_writing_mode = False
|
|
self.ltr = True
|
|
self.rtl = False
|
|
|
|
def flow_x(self):
|
|
if self.rtl:
|
|
return -window.pageXOffset
|
|
return window.pageXOffset
|
|
|
|
def flow_y(self):
|
|
return window.pageYOffset
|
|
|
|
def flow_window_scroll_pos(self):
|
|
return window.pageXOffset, window.pageYOffset
|
|
|
|
def inline_pos(self):
|
|
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):
|
|
if self.rtl:
|
|
window.scrollTo(-x,y)
|
|
else:
|
|
window.scrollTo(x, y)
|
|
|
|
def scroll_to_in_inline_direction(self, pos, preserve_other):
|
|
# Lines flow vertically, so inline is vertical.
|
|
if self.vertical_writing_mode:
|
|
self.scroll_to(self.x() if preserve_other else 0, pos)
|
|
else:
|
|
self.scroll_to(pos, self.y() if preserve_other else 0)
|
|
|
|
def scroll_to_in_block_direction(self, pos, preserve_other):
|
|
# In horizontal modes, the block direction is vertical.
|
|
if self.horizontal_writing_mode:
|
|
self.scroll_to(self.x() if preserve_other else 0, pos)
|
|
# In vertical modes, the block direction is horizontal.
|
|
else:
|
|
self.scroll_to(pos, self.y() if preserve_other else 0)
|
|
|
|
def flow_scroll_into_view(self, elem):
|
|
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):
|
|
pass
|
|
|
|
def flow___reset_transforms(self):
|
|
pass
|
|
|
|
def flow_content_size(self):
|
|
return document.documentElement.scrollWidth, document.documentElement.scrollHeight
|
|
|
|
def paged_content_inline_size(self):
|
|
w, h = self.content_size()
|
|
return w if self.horizontal_writing_mode else h
|
|
|
|
def paged_content_block_size(self):
|
|
w, h = self.content_size()
|
|
return h if self.horizontal_writing_mode else w
|
|
|
|
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):
|
|
self.window_width_from_parent = w
|
|
self.window_height_from_parent = h
|
|
|
|
def width(self):
|
|
return window.innerWidth
|
|
|
|
def height(self):
|
|
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
|
|
# mode of ScrollViewport.
|
|
def viewport_to_document(self, x, y, doc):
|
|
# Convert x, y from the viewport (window) co-ordinate system to the
|
|
# document (body) co-ordinate system
|
|
doc = doc or window.document
|
|
topdoc = window.document
|
|
while doc is not topdoc and doc.defaultView:
|
|
# We are in a frame
|
|
frame = doc.defaultView.frameElement
|
|
rect = frame.getBoundingClientRect()
|
|
x += rect.left
|
|
y += rect.top
|
|
doc = frame.ownerDocument
|
|
wx, wy = self.window_scroll_pos()
|
|
x += wx
|
|
y += wy
|
|
if self.rtl:
|
|
x *= -1
|
|
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, doc):
|
|
if self.horizontal_writing_mode:
|
|
return self.viewport_to_document(inline, block, doc)
|
|
return self.viewport_to_document(block, inline, doc)
|
|
|
|
|
|
class IOSScrollViewport(ScrollViewport):
|
|
|
|
def width(self):
|
|
return self.window_width_from_parent or window.innerWidth
|
|
|
|
def height(self):
|
|
return self.window_height_from_parent or window.innerHeight
|
|
|
|
def _scroll_implementation(self, x, y):
|
|
if x is 0 and y is 0:
|
|
document.documentElement.style.transform = 'none'
|
|
else:
|
|
x *= -1
|
|
y *= -1
|
|
document.documentElement.style.transform = f'translateX({x}px) translateY({y}px)'
|
|
|
|
def paged_scroll_to(self, x, y):
|
|
x = x or 0
|
|
y = y or 0
|
|
if self.ltr:
|
|
self._scroll_implementation(x, y)
|
|
else:
|
|
self._scroll_implementation(-x, y)
|
|
boss = get_boss()
|
|
if boss:
|
|
boss.onscroll()
|
|
|
|
def transform_val(self, y):
|
|
raw = document.documentElement.style.transform
|
|
if not raw or raw is 'none':
|
|
return 0
|
|
idx = raw.lastIndexOf('(') if y else raw.indexOf('(')
|
|
raw = raw[idx + 1:]
|
|
ans = parseInt(raw)
|
|
if isNaN(ans):
|
|
ans = 0
|
|
return ans
|
|
|
|
def paged_x(self):
|
|
ans = self.transform_val()
|
|
if self.ltr:
|
|
ans *= -1
|
|
return ans
|
|
|
|
def paged_y(self):
|
|
return -1 * self.transform_val(True)
|
|
|
|
def paged_scroll_into_view(self, elem):
|
|
left = elem.offsetLeft
|
|
if left is None:
|
|
return # element has display: none
|
|
elem.scrollIntoView()
|
|
window.scrollTo(0, 0)
|
|
p = elem.offsetParent
|
|
while p:
|
|
left += p.offsetLeft
|
|
p = p.offsetParent
|
|
# left -= window_width() // 2
|
|
self._scroll_implementation(max(0, left), 0)
|
|
|
|
def paged_window_scroll_pos(self):
|
|
return self.transform_val(), self.transform_val(True)
|
|
|
|
def paged_content_size(self):
|
|
w = document.documentElement.scrollWidth
|
|
h = document.documentElement.scrollHeight
|
|
return w - self.transform_val(), h - self.transform_val(True)
|
|
|
|
def paged___reset_transforms(self):
|
|
s = document.documentElement.style
|
|
if s.transform is not 'none':
|
|
s.transform = 'none'
|
|
|
|
def paged_reset_globals(self):
|
|
self.__reset_transforms()
|
|
|
|
|
|
if 1 < ios_major_version < 15:
|
|
scroll_viewport = IOSScrollViewport()
|
|
else:
|
|
scroll_viewport = ScrollViewport()
|
|
for attr in FUNCTIONS:
|
|
if not scroll_viewport['paged_' + attr]:
|
|
scroll_viewport['paged_' + attr] = scroll_viewport[attr]
|
|
viewport_mode_changer(scroll_viewport.set_mode)
|
|
|
|
|
|
def get_unit_size_in_pixels(unit):
|
|
d = document.createElement('span')
|
|
d.style.position = 'absolute'
|
|
d.style.visibility = 'hidden'
|
|
d.style.width = f'1{unit}'
|
|
d.style.fontSize = f'1{unit}'
|
|
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)
|
|
ans = d.clientWidth
|
|
document.body.removeChild(d)
|
|
return ans
|
|
|
|
|
|
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 = max(2, d.clientWidth)
|
|
document.body.removeChild(d)
|
|
return rem_size.ans
|
|
|
|
def line_height(reset):
|
|
if reset:
|
|
line_height.ans = None
|
|
return
|
|
if not line_height.ans:
|
|
ds = window.getComputedStyle(document.body)
|
|
try:
|
|
# will fail if line-height = "normal"
|
|
lh = float(ds.lineHeight)
|
|
except:
|
|
try:
|
|
lh = 1.2 * float(ds.fontSize)
|
|
except:
|
|
lh = 15
|
|
line_height.ans = max(5, lh)
|
|
return line_height.ans
|