11local BD = require (" ui/bidi" )
2+ local Blitbuffer = require (" ffi/blitbuffer" )
23local Device = require (" device" )
34local DoubleSpinWidget = require (" ui/widget/doublespinwidget" )
45local Geom = require (" ui/geometry" )
@@ -22,6 +23,128 @@ local math_max = math.max
2223local math_min = math.min
2324local math_floor = math.floor
2425
26+ -- Minimal overlay that draws the crosshairs without touching the document
27+ -- render layer. Captures the page as its background on first paint, then
28+ -- restores only the previous indicator region before drawing the new one.
29+ local IndicatorOverlay = InputContainer :extend {
30+ parent_ui = nil ,
31+ handleEvent = true ,
32+ indicator_rect = nil ,
33+ _prev_rect = nil ,
34+ _saved_bb = nil ,
35+ covers_fullscreen = false ,
36+ }
37+
38+ local function getIndicatorSaveRect (rect , max_w , max_h )
39+ if not rect then return nil end
40+ local save_r = rect :copy ()
41+ local bt2 = math_floor (Size .border .thick / 2 )
42+ save_r .x = math_floor (save_r .x - bt2 )
43+ save_r .y = math_floor (save_r .y - bt2 )
44+ save_r .w = save_r .w + Size .border .thick * 2
45+ save_r .h = save_r .h + Size .border .thick * 2
46+ save_r = save_r :intersect (Geom :new { x = 0 , y = 0 , w = max_w , h = max_h })
47+ if not save_r or save_r .w <= 0 or save_r .h <= 0 then
48+ return nil
49+ end
50+ return save_r
51+ end
52+
53+ local function getIndicatorDirtyRect (old_rect , new_rect , max_w , max_h )
54+ local old_save = getIndicatorSaveRect (old_rect , max_w , max_h )
55+ local new_save = getIndicatorSaveRect (new_rect , max_w , max_h )
56+ if old_save and new_save then
57+ return Geom .boundingBox ({ old_save , new_save })
58+ end
59+ return old_save or new_save
60+ end
61+
62+ function IndicatorOverlay :freeSavedBB ()
63+ if self ._saved_bb then
64+ self ._saved_bb :free ()
65+ self ._saved_bb = nil
66+ end
67+ self ._prev_rect = nil
68+ end
69+
70+ function IndicatorOverlay :handleEvent (event )
71+ if not event or not event .handler then return false end
72+ -- Only forward input events that would otherwise be swallowed by us, being
73+ -- the topmost layer. Broadcast events reach parent_ui directly via UIManager.
74+ local input_events = {
75+ onKeyPress = true ,
76+ onKeyRepeat = true ,
77+ onKeyRelease = true ,
78+ onGesture = true ,
79+ onPan = true , -- mouse wheel pan via sendEvent
80+ }
81+ if input_events [event .handler ] and self .parent_ui then
82+ return self .parent_ui :handleEvent (event )
83+ end
84+ return false
85+ end
86+
87+ function IndicatorOverlay :drawCrosshairs (bb , rect )
88+ if not rect then return end
89+ bb :invertRect (
90+ rect .x ,
91+ math.floor (rect .y + rect .h / 2 - Size .border .thick / 2 ),
92+ rect .w ,
93+ Size .border .thick
94+ )
95+ bb :invertRect (
96+ math.floor (rect .x + rect .w / 2 - Size .border .thick / 2 ),
97+ rect .y ,
98+ Size .border .thick ,
99+ rect .h
100+ )
101+ end
102+
103+ function IndicatorOverlay :getSize ()
104+ return self .dimen or Screen :getSize ()
105+ end
106+
107+ function IndicatorOverlay :paintTo (bb , x , y , is_dirty )
108+ -- If is_dirty is nil, the parent ReaderUI painted over us with new content.
109+ -- The background we saved is now invalid, so we clear it.
110+ if is_dirty == nil then
111+ self :freeSavedBB ()
112+ end
113+
114+ -- Restore previous unblemished page background
115+ if self ._prev_rect and self ._saved_bb then
116+ local r = self ._prev_rect
117+ bb :blitFrom (self ._saved_bb , r .x , r .y , 0 , 0 , r .w , r .h )
118+ end
119+
120+ if self .indicator_rect then
121+ local r = self .indicator_rect
122+ local save_r = getIndicatorSaveRect (r , bb :getWidth (), bb :getHeight ())
123+ if save_r and self .dimen then
124+ save_r = save_r :intersect (self .dimen )
125+ end
126+ if not save_r or save_r .w <= 0 or save_r .h <= 0 then
127+ self ._prev_rect = nil
128+ return
129+ end
130+
131+ -- Resize the saved Blitbuffer if necessary
132+ if not self ._saved_bb or self ._saved_bb :getWidth () < save_r .w or self ._saved_bb :getHeight () < save_r .h then
133+ self :freeSavedBB ()
134+ self ._saved_bb = Blitbuffer .new (save_r .w , save_r .h , bb :getType ())
135+ end
136+
137+ -- Copy clean background from screen
138+ self ._saved_bb :blitFrom (bb , 0 , 0 , save_r .x , save_r .y , save_r .w , save_r .h )
139+
140+ -- Draw the crosshair natively overriding the background
141+ self :drawCrosshairs (bb , self .indicator_rect )
142+ self ._prev_rect = save_r
143+ else
144+ self ._prev_rect = nil
145+ end
146+ end
147+
25148local ReaderKeySelection = InputContainer :extend {}
26149
27150function ReaderKeySelection :init ()
48171
49172function ReaderKeySelection :onSetDimensions (dimen )
50173 self .screen_w , self .screen_h = dimen .w , dimen .h
174+ if self ._indicator_overlay then
175+ local overlay_rect = getIndicatorSaveRect (self ._current_indicator_pos , dimen .w , dimen .h )
176+ self ._indicator_overlay .dimen = overlay_rect or Geom :new { x = 0 , y = 0 , w = dimen .w , h = dimen .h }
177+ self ._indicator_overlay :freeSavedBB ()
178+ end
51179end
52180
53181function ReaderKeySelection :registerKeyEvents ()
@@ -229,6 +357,11 @@ function ReaderKeySelection:isActive()
229357 return self ._current_indicator_pos ~= nil
230358end
231359
360+ function ReaderKeySelection :clearOverlay ()
361+ if not self ._indicator_overlay then return end
362+ self ._indicator_overlay :freeSavedBB ()
363+ end
364+
232365function ReaderKeySelection :startHighlightIndicator ()
233366 -- disable long-press icon (poke-ball), as it is triggered constantly due to NT devices needing a workaround for text selection to work.
234367 self .ui .highlight .long_hold_reached_action = function () end
@@ -243,10 +376,24 @@ function ReaderKeySelection:startHighlightIndicator()
243376 rect .h = rect .w
244377 end
245378 self ._current_indicator_pos = rect
379+
380+ -- Compute padded saved region (match paintTo padding)
381+ local max_w = self .screen_w or Screen :getWidth ()
382+ local max_h = self .screen_h or Screen :getHeight ()
383+ local save_r = getIndicatorSaveRect (rect , max_w , max_h )
384+ -- Fallback to minimal rect if intersection collapsed
385+ if not save_r then
386+ save_r = Geom :new { x = math.floor (rect .x ), y = math.floor (rect .y ), w = rect .w , h = rect .h }
387+ end
388+ self ._indicator_overlay = IndicatorOverlay :new {
389+ dimen = Geom :new { x = save_r .x , y = save_r .y , w = save_r .w , h = save_r .h },
390+ parent_ui = self .ui ,
391+ }
392+ UIManager :show (self ._indicator_overlay )
246393 if self .ui .paging then
247394 self ._last_indicator_move_args = {dx = 0 , dy = 0 , distance = 0 , time = time :now ()}
248- self .view . highlight . indicator = rect
249- UIManager :setDirty (self .dialog , " ui" , rect )
395+ self ._indicator_overlay . indicator_rect = rect
396+ UIManager :setDirty (self ._indicator_overlay , " ui" , rect )
250397 return true
251398 end
252399 local center_x = rect .x + rect .w * 0.5
@@ -285,6 +432,11 @@ function ReaderKeySelection:stopHighlightIndicator(need_clear_selection)
285432 self ._edge_dx , self ._edge_dy = nil , nil
286433 self ._last_move_was_quick_move = nil
287434 self ._previous_indicator_word = nil
435+ if self ._indicator_overlay then
436+ self ._indicator_overlay :freeSavedBB ()
437+ UIManager :close (self ._indicator_overlay )
438+ self ._indicator_overlay = nil
439+ end
288440 self ._last_indicator_move_args = nil
289441 UIManager :setDirty (self .dialog , " ui" , rect )
290442 if need_clear_selection then
@@ -421,6 +573,14 @@ function ReaderKeySelection:pageTurnDuringSelection()
421573 self ._edge_dx , self ._edge_dy = nil , nil
422574 self ._previous_indicator_word = nil
423575 local last_pos = self ._current_indicator_pos
576+ if self ._indicator_overlay then
577+ local old_dirty = getIndicatorSaveRect (last_pos , self .screen_w , self .screen_h )
578+ self ._indicator_overlay :freeSavedBB ()
579+ self ._indicator_overlay .indicator_rect = nil
580+ if old_dirty then
581+ UIManager :setDirty (self .dialog , " ui" , old_dirty )
582+ end
583+ end
424584 local target_x = last_pos .x + last_pos .w * 0.5
425585 local target_y = last_pos .y + last_pos .h * 0.5
426586
@@ -577,10 +737,21 @@ function ReaderKeySelection:_setIndicatorToWord(word)
577737end
578738
579739function ReaderKeySelection :_setIndicatorRect (rect )
580- UIManager : setDirty ( self . dialog , " fast " , self ._current_indicator_pos )
740+ local old_rect = self ._current_indicator_pos
581741 self ._current_indicator_pos = rect
582- self .view .highlight .indicator = self ._current_indicator_pos
583- UIManager :setDirty (self .dialog , " fast" , self ._current_indicator_pos )
742+ if not self ._indicator_overlay then
743+ logger .warn (" ReaderKeySelection: _setIndicatorRect: no overlay" )
744+ return
745+ end
746+ logger .dbg (" ReaderKeySelection: _setIndicatorRect: dirtying overlay, rect=" , rect )
747+ self ._indicator_overlay .indicator_rect = rect
748+ local dirty = getIndicatorDirtyRect (old_rect , rect , self .screen_w , self .screen_h )
749+ if dirty then
750+ self ._indicator_overlay .dimen = dirty
751+ UIManager :setDirty (self ._indicator_overlay , " fast" , dirty )
752+ else
753+ UIManager :setDirty (self ._indicator_overlay , " fast" , rect )
754+ end
584755end
585756
586757function ReaderKeySelection :_getWordAnchorCoordinates (word )
0 commit comments