@@ -203,6 +203,17 @@ fn render_view(view: &mut BlitzView) {
203203 render_h,
204204 ) ;
205205
206+ // Only swap the frame when pixels actually changed. This prevents
207+ // visual flicker during the resource drain phase when resolve() is
208+ // called periodically but no new resources have arrived yet.
209+ if view. last_frame . image_width ( ) == render_w
210+ && view. last_frame . image_height ( ) == render_h
211+ && * view. last_frame . pixels ( ) == buffer
212+ {
213+ view. needs_render = false ;
214+ return ;
215+ }
216+
206217 view. last_frame = ImageInfo :: new ( buffer, PixelFormat :: Rgba , render_w, render_h) ;
207218 view. needs_render = false ;
208219}
@@ -211,17 +222,16 @@ fn render_view(view: &mut BlitzView) {
211222/// At 10ms per tick this gives ~30s for sub-resources to arrive.
212223const RESOURCE_TICK_BUDGET : u32 = 3000 ;
213224
214- /// Drain completed resource fetches and re-resolve if something changed.
215- /// Only called while `resource_ticks > 0` (after a goto).
216- fn drain_and_resolve ( view : & mut BlitzView ) -> bool {
217- let doc = match view. document . as_mut ( ) {
218- Some ( d) => d,
219- None => return false ,
220- } ;
221- let height_before = doc. root_element ( ) . final_layout . size . height ;
222- doc. resolve ( 0.0 ) ;
223- let height_after = doc. root_element ( ) . final_layout . size . height ;
224- height_before != height_after
225+ /// How often (in ticks) to actually call resolve() during the drain phase.
226+ /// resolve() is expensive (full Stylo + Taffy layout pass), so we throttle it.
227+ /// At ~10ms per tick, 100 ticks ≈ 1 second between resolve calls.
228+ const RESOLVE_INTERVAL : u32 = 100 ;
229+
230+ /// Drain completed resource fetches and re-resolve the document.
231+ fn drain_and_resolve ( view : & mut BlitzView ) {
232+ if let Some ( ref mut doc) = view. document {
233+ doc. resolve ( 0.0 ) ;
234+ }
225235}
226236
227237impl Engine for Blitz {
@@ -239,7 +249,8 @@ impl Engine for Blitz {
239249 for view in & mut self . views {
240250 if view. resource_ticks > 0 {
241251 view. resource_ticks -= 1 ;
242- if drain_and_resolve ( view) {
252+ if view. resource_ticks % RESOLVE_INTERVAL == 0 {
253+ drain_and_resolve ( view) ;
243254 view. needs_render = true ;
244255 }
245256 }
@@ -398,9 +409,9 @@ impl Engine for Blitz {
398409 page_x : point. x ,
399410 page_y : doc_y,
400411 screen_x : point. x ,
401- screen_y : doc_y ,
412+ screen_y : point . y ,
402413 client_x : point. x ,
403- client_y : doc_y ,
414+ client_y : point . y ,
404415 } ,
405416 button : MouseEventButton :: Main ,
406417 buttons : MouseEventButtons :: Primary ,
@@ -415,9 +426,6 @@ impl Engine for Blitz {
415426 let doc_y = point. y + view. scroll_y ;
416427 doc. set_hover_to ( point. x , doc_y) ;
417428 }
418- // Update cursor icon without re-rendering — matching litehtml
419- // behaviour. A full re-render for :hover CSS would be too
420- // expensive with CPU rasterization.
421429 let doc_cursor = view. document . as_ref ( ) . and_then ( |d| d. get_cursor ( ) ) ;
422430 let shell_cursor = * view. cursor_icon . lock ( ) . unwrap ( ) ;
423431 let icon = doc_cursor. unwrap_or ( shell_cursor) ;
@@ -434,9 +442,9 @@ impl Engine for Blitz {
434442 page_x : point. x ,
435443 page_y : doc_y,
436444 screen_x : point. x ,
437- screen_y : doc_y ,
445+ screen_y : point . y ,
438446 client_x : point. x ,
439- client_y : doc_y ,
447+ client_y : point . y ,
440448 } ,
441449 button : MouseEventButton :: Main ,
442450 buttons : MouseEventButtons :: None ,
@@ -532,6 +540,33 @@ impl Engine for Blitz {
532540 self . find_view ( id) . content_height
533541 }
534542
543+ fn scroll_to_fragment ( & mut self , id : ViewId , fragment : & str ) -> bool {
544+ let view = self . find_view_mut ( id) ;
545+ let doc = match view. document . as_ref ( ) {
546+ Some ( d) => d,
547+ None => return false ,
548+ } ;
549+
550+ // Try #id first (fast HashMap lookup), then [name="fragment"] via CSS selector.
551+ let node_id = doc. get_element_by_id ( fragment) . or_else ( || {
552+ let quoted = fragment. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " ) ;
553+ doc. query_selector ( & format ! ( "[name=\" {quoted}\" ]" ) )
554+ . ok ( )
555+ . flatten ( )
556+ } ) ;
557+
558+ if let Some ( nid) = node_id {
559+ if let Some ( node) = doc. get_node ( nid) {
560+ let pos = node. absolute_position ( 0.0 , 0.0 ) ;
561+ let max_scroll = ( view. content_height - view. size . height as f32 ) . max ( 0.0 ) ;
562+ view. scroll_y = pos. y . clamp ( 0.0 , max_scroll) ;
563+ return true ;
564+ }
565+ }
566+
567+ false
568+ }
569+
535570 fn take_anchor_click ( & mut self , id : ViewId ) -> Option < String > {
536571 self . find_view_mut ( id) . nav_capture . lock ( ) . unwrap ( ) . take ( )
537572 }
0 commit comments