@@ -53,27 +53,50 @@ impl SelectionEndpoint {
5353 }
5454}
5555
56+ /// Cached result of a document-order comparison between two elements.
57+ #[ derive( Clone ) ]
58+ struct OrderCache {
59+ a : * mut crate :: sys:: lh_element_t ,
60+ b : * mut crate :: sys:: lh_element_t ,
61+ a_before_b : bool ,
62+ }
63+
5664/// Text selection state for a litehtml document.
5765///
58- /// Holds the start and end endpoints of a selection, plus cached highlight
59- /// rectangles. Endpoints contain raw element pointers valid while the parent
60- /// `Document` is alive — the caller must ensure the document outlives this struct .
61- pub struct Selection {
66+ /// The `'doc` lifetime ties this selection to its parent [`Document`], preventing
67+ /// use-after-free if the document is dropped while the selection holds element
68+ /// pointers. Use [`Selection::for_document`] to create a lifetime-bound selection .
69+ pub struct Selection < ' doc > {
6270 start : Option < SelectionEndpoint > ,
6371 end : Option < SelectionEndpoint > ,
6472 rectangles : Vec < Position > ,
73+ order_cache : Option < OrderCache > ,
74+ _doc : PhantomData < & ' doc ( ) > ,
6575}
6676
67- impl Selection {
68- /// Create an empty (inactive) selection.
77+ impl < ' doc > Selection < ' doc > {
78+ /// Create an empty (inactive) selection without lifetime enforcement.
79+ ///
80+ /// Prefer [`Selection::for_document`] to tie the selection lifetime to
81+ /// a specific document, preventing use-after-free at compile time.
6982 pub fn new ( ) -> Self {
7083 Self {
7184 start : None ,
7285 end : None ,
7386 rectangles : Vec :: new ( ) ,
87+ order_cache : None ,
88+ _doc : PhantomData ,
7489 }
7590 }
7691
92+ /// Create an empty selection tied to a document's lifetime.
93+ ///
94+ /// The returned `Selection` cannot outlive `doc`, enforced by the compiler.
95+ /// The document is NOT borrowed persistently — only the lifetime is captured.
96+ pub fn for_document ( _doc : & ' doc Document < ' _ > ) -> Self {
97+ Self :: new ( )
98+ }
99+
77100 /// Begin a selection at document coordinates `(x, y)`.
78101 ///
79102 /// `measure_text` should return the pixel width of a string rendered with
@@ -119,6 +142,7 @@ impl Selection {
119142 self . start = None ;
120143 self . end = None ;
121144 self . rectangles . clear ( ) ;
145+ self . order_cache = None ;
122146 }
123147
124148 /// Returns `true` if there is an active selection with both start and end.
@@ -133,8 +157,8 @@ impl Selection {
133157 let start = self . start . as_ref ( ) ?;
134158 let end = self . end . as_ref ( ) ?;
135159
136- // Normalize into document order
137- let ( first, second) = normalize_endpoints ( start, end) ;
160+ // Normalize into document order (use cache if available)
161+ let ( first, second) = normalize_endpoints ( start, end, & self . order_cache ) ;
138162 let first_el = first. element ( ) ;
139163 let second_el = second. element ( ) ;
140164
@@ -183,8 +207,24 @@ impl Selection {
183207 _ => return ,
184208 } ;
185209
210+ // Update order cache if endpoints changed
211+ if start. element != end. element {
212+ let needs_update = self
213+ . order_cache
214+ . as_ref ( )
215+ . map_or ( true , |c| c. a != start. element || c. b != end. element ) ;
216+ if needs_update {
217+ let a_before_b = is_before ( & start. element ( ) , & end. element ( ) ) ;
218+ self . order_cache = Some ( OrderCache {
219+ a : start. element ,
220+ b : end. element ,
221+ a_before_b,
222+ } ) ;
223+ }
224+ }
225+
186226 // Normalize into document order
187- let ( first, second) = normalize_endpoints ( start, end) ;
227+ let ( first, second) = normalize_endpoints ( start, end, & self . order_cache ) ;
188228
189229 if first. element == second. element {
190230 let el = first. element ( ) ;
@@ -234,7 +274,7 @@ impl Selection {
234274 }
235275}
236276
237- impl Default for Selection {
277+ impl Default for Selection < ' _ > {
238278 fn default ( ) -> Self {
239279 Self :: new ( )
240280 }
@@ -261,20 +301,29 @@ fn is_before(a: &Element<'_>, b: &Element<'_>) -> bool {
261301}
262302
263303/// Normalize user-order endpoints into document order: returns (first, second).
304+ ///
305+ /// Uses the cached order result when available to avoid repeated DOM walks.
264306fn normalize_endpoints < ' a > (
265307 a : & ' a SelectionEndpoint ,
266308 b : & ' a SelectionEndpoint ,
309+ cache : & Option < OrderCache > ,
267310) -> ( & ' a SelectionEndpoint , & ' a SelectionEndpoint ) {
268311 if a. element == b. element {
269312 if a. char_index <= b. char_index {
270313 ( a, b)
271314 } else {
272315 ( b, a)
273316 }
274- } else if is_before ( & a. element ( ) , & b. element ( ) ) {
275- ( a, b)
276317 } else {
277- ( b, a)
318+ let a_before_b = cache
319+ . as_ref ( )
320+ . filter ( |c| c. a == a. element && c. b == b. element )
321+ . map_or_else ( || is_before ( & a. element ( ) , & b. element ( ) ) , |c| c. a_before_b ) ;
322+ if a_before_b {
323+ ( a, b)
324+ } else {
325+ ( b, a)
326+ }
278327 }
279328}
280329
@@ -346,6 +395,8 @@ fn hit_test_char(
346395
347396/// Find which character index corresponds to pixel offset `target_x` within
348397/// the given text rendered with `font`.
398+ ///
399+ /// Builds the prefix string incrementally to avoid O(n) allocations per call.
349400fn find_char_at_x (
350401 measure_text : & MeasureTextFn < ' _ > ,
351402 text : & str ,
@@ -356,21 +407,23 @@ fn find_char_at_x(
356407 return 0 ;
357408 }
358409
359- let chars : Vec < char > = text. chars ( ) . collect ( ) ;
410+ let mut prefix = String :: with_capacity ( text. len ( ) ) ;
360411 let mut prev_width = 0.0f32 ;
412+ let mut count = 0 ;
361413
362- for i in 0 ..chars. len ( ) {
363- let prefix: String = chars[ ..=i] . iter ( ) . collect ( ) ;
414+ for ch in text. chars ( ) {
415+ prefix. push ( ch) ;
416+ count += 1 ;
364417 let width = measure_text ( & prefix, font) ;
365418 let midpoint = ( prev_width + width) / 2.0 ;
366419
367420 if target_x < midpoint {
368- return i ;
421+ return count - 1 ;
369422 }
370423 prev_width = width;
371424 }
372425
373- chars . len ( )
426+ count
374427}
375428
376429// ---------------------------------------------------------------------------
@@ -482,12 +535,18 @@ fn closest_text_leaf<'a>(el: &Element<'a>, target_x: f32, target_y: f32) -> Opti
482535 . map ( |( el, _) | el)
483536}
484537
538+ /// Maximum ancestor levels to traverse before giving up.
539+ /// Prevents infinite loops on malformed DOMs.
540+ const MAX_TREE_DEPTH : usize = 256 ;
541+
485542/// Walk to the next text leaf after `el`, stopping before `stop`.
486543/// Walks up to the parent, then to the next sibling, then descends.
544+ /// Gives up after [`MAX_TREE_DEPTH`] ancestor levels to guard against
545+ /// malformed or extremely deep DOMs.
487546fn next_text_leaf < ' a > ( el : & Element < ' a > , stop : & Element < ' a > ) -> Option < Element < ' a > > {
488547 let mut current_ptr = el. as_ptr ( ) ;
489548
490- loop {
549+ for _ in 0 .. MAX_TREE_DEPTH {
491550 let current = Element {
492551 ptr : current_ptr,
493552 _phantom : PhantomData ,
@@ -525,6 +584,8 @@ fn next_text_leaf<'a>(el: &Element<'a>, stop: &Element<'a>) -> Option<Element<'a
525584 // No more siblings at this level, walk up
526585 current_ptr = parent. as_ptr ( ) ;
527586 }
587+
588+ None
528589}
529590
530591// ---------------------------------------------------------------------------
0 commit comments