Skip to content

Commit a8fbcf5

Browse files
committed
fix: blitz engine resolve throttling, coordinate handling, and fragment navigation
1 parent d1cc048 commit a8fbcf5

1 file changed

Lines changed: 54 additions & 19 deletions

File tree

src/engines/blitz.rs

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
212223
const 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

227237
impl 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

Comments
 (0)