diff --git a/src/pyj/read_book/anchor_visibility.pyj b/src/pyj/read_book/anchor_visibility.pyj new file mode 100644 index 0000000000..540eee13be --- /dev/null +++ b/src/pyj/read_book/anchor_visibility.pyj @@ -0,0 +1,40 @@ +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2023, Kovid Goyal +from __python__ import bound_methods, hash_literals + +from read_book.globals import current_layout_mode, get_boss +from read_book.viewport import scroll_viewport + +anchor_position_cache = {} + +def invalidate_anchor_position_cache(): + nonlocal anchor_position_cache + anchor_position_cache = { + 'layout_mode': current_layout_mode(), 'width': scroll_viewport.width(), 'height': scroll_viewport.height(), + 'positions': v'{}', + } + + +def ensure_anchor_cache_valid(): + a = anchor_position_cache + if a.layout_mode is not current_layout_mode() or a.width is not scroll_viewport.width() or a.height is not scroll_viewport.height(): + invalidate_anchor_position_cache() + + +def position_for_anchor(anchor): + ensure_anchor_cache_valid() + cache = anchor_position_cache.positions + val = cache[anchor] + if val?: + return val + anchor_funcs = get_boss().anchor_funcs + elem = document.getElementById(anchor) + val = anchor_funcs.pos_for_elem(elem) if elem else anchor_funcs.pos_for_elem() + cache[anchor] = val + return val + + +def is_anchor_on_screen(anchor): + pos = position_for_anchor(anchor) + anchor_funcs = get_boss().anchor_funcs + return anchor_funcs.visibility(pos) is 0 diff --git a/src/pyj/read_book/iframe.pyj b/src/pyj/read_book/iframe.pyj index 8a5262044a..4bd699d854 100644 --- a/src/pyj/read_book/iframe.pyj +++ b/src/pyj/read_book/iframe.pyj @@ -9,6 +9,7 @@ from range_utils import ( all_annots_in_selection, highlight_associated_with_selection, last_span_for_crw, reset_highlight_counter, select_crw, unwrap_all_crw, unwrap_crw, wrap_text_in_range ) +from read_book.anchor_visibility import invalidate_anchor_position_cache from read_book.cfi import cfi_for_selection, range_from_cfi from read_book.extract import get_elements from read_book.find import ( @@ -402,6 +403,7 @@ class IframeBoss: if cfi: self.jump_to_cfi('/' + cfi) self.update_cfi() + invalidate_anchor_position_cache() self.update_toc_position(True) def number_of_columns_changed(self, data): @@ -435,6 +437,7 @@ class IframeBoss: self.apply_highlights_on_load(self.highlights_to_apply) self.highlights_to_apply = None apply_settings() + invalidate_anchor_position_cache() fix_fullscreen_svg_images() self.do_layout(self.is_titlepage) if self.mathjax: @@ -549,7 +552,7 @@ class IframeBoss: 'update_progress_frac', progress_frac=pf, file_progress_frac=fpf, page_counts=page_counts()) def update_toc_position(self, recalculate_toc_anchor_positions): - visible_anchors = update_visible_toc_anchors(self.book.manifest.toc_anchor_map, self.anchor_funcs, bool(recalculate_toc_anchor_positions)) + visible_anchors = update_visible_toc_anchors(self.book.manifest.toc_anchor_map, bool(recalculate_toc_anchor_positions)) self.send_message('update_toc_position', visible_anchors=visible_anchors) def onscroll(self): @@ -815,7 +818,7 @@ class IframeBoss: sel = window.getSelection() if not sel.rangeCount: return - anchor_before = find_anchor_before_range(sel.getRangeAt(0), self.book.manifest.toc_anchor_map, self.anchor_funcs) + anchor_before = find_anchor_before_range(sel.getRangeAt(0), self.book.manifest.toc_anchor_map) text = sel.toString() bounds = cfi_for_selection() style = highlight_style_as_css(style, opts.is_dark_theme, opts.color_scheme.foreground) diff --git a/src/pyj/read_book/toc.pyj b/src/pyj/read_book/toc.pyj index 1ab556e161..f930014de7 100644 --- a/src/pyj/read_book/toc.pyj +++ b/src/pyj/read_book/toc.pyj @@ -2,14 +2,19 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal from __python__ import hash_literals -from complete import create_search_bar -from dom import set_css, svgicon, ensure_id from elementmaker import E + +from complete import create_search_bar +from dom import ensure_id, set_css, svgicon from gettext import gettext as _ from modals import error_dialog -from widgets import create_tree, find_text_in_tree, scroll_tree_item_into_view -from read_book.globals import toc_anchor_map, set_toc_anchor_map, current_spine_item, current_layout_mode, current_book +from read_book.anchor_visibility import position_for_anchor +from read_book.globals import ( + current_book, current_layout_mode, current_spine_item, get_boss, set_toc_anchor_map, + toc_anchor_map +) from read_book.viewport import scroll_viewport +from widgets import create_tree, find_text_in_tree, scroll_tree_item_into_view def update_visible_toc_nodes(visible_anchors): @@ -198,20 +203,16 @@ def create_toc_panel(book, container, onclick): toc_panel.style.flexShrink = '1' toc_panel.style.overflow = 'auto' -def recalculate_toc_anchor_positions(tam, anchor_funcs): +def recalculate_toc_anchor_positions(tam): name = current_spine_item().name am = {} anchors = v'[]' pos_map = {} for i, anchor in enumerate(tam[name] or v'[]'): - val = anchor_funcs.pos_for_elem() - if anchor.frag: - elem = document.getElementById(anchor.frag) - if elem: - val = anchor_funcs.pos_for_elem(elem) - am[anchor.id] = val + am[anchor.id] = position_for_anchor(anchor.frag) anchors.push(anchor.id) pos_map[anchor.id] = i + anchor_funcs = get_boss().anchor_funcs # stable sort by position in document anchors.sort(def (a, b): return anchor_funcs.cmp(am[a], am[b]) or (pos_map[a] - pos_map[b]);) @@ -220,20 +221,21 @@ def recalculate_toc_anchor_positions(tam, anchor_funcs): return current_map -def current_toc_anchor_map(tam, anchor_funcs): +def current_toc_anchor_map(tam): current_map = toc_anchor_map() if not (current_map and current_map.layout_mode is current_layout_mode() and current_map.width is scroll_viewport.width() and current_map.height is scroll_viewport.height()): - current_map = recalculate_toc_anchor_positions(tam, anchor_funcs) + current_map = recalculate_toc_anchor_positions(tam) return current_map -def update_visible_toc_anchors(toc_anchor_map, anchor_funcs, recalculate): +def update_visible_toc_anchors(toc_anchor_map, recalculate): if recalculate: - recalculate_toc_anchor_positions(toc_anchor_map, anchor_funcs) - tam = current_toc_anchor_map(toc_anchor_map, anchor_funcs) + recalculate_toc_anchor_positions(toc_anchor_map) + tam = current_toc_anchor_map(toc_anchor_map) before = after = None visible_anchors = {} has_visible = False + anchor_funcs = get_boss().anchor_funcs for anchor_id in tam.sorted_anchors: pos = tam.pos_map[anchor_id] @@ -250,10 +252,10 @@ def update_visible_toc_anchors(toc_anchor_map, anchor_funcs, recalculate): return {'visible_anchors':visible_anchors, 'has_visible':has_visible, 'before':before, 'after':after, 'sorted_anchors':tam.sorted_anchors} -def find_anchor_before_range(r, toc_anchor_map, anchor_funcs): +def find_anchor_before_range(r, toc_anchor_map): name = current_spine_item().name prev_anchor = None - tam = current_toc_anchor_map(toc_anchor_map, anchor_funcs) + tam = current_toc_anchor_map(toc_anchor_map) anchors = toc_anchor_map[name] if anchors: amap = {x.id: x for x in anchors}