Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,11 +520,27 @@ func (b *Browser) CloseTab(i int) {
b.changed()
}

// Deliver hands the widget a finished render for target. When target matches the
// active tab's current URL the render (pixels + dimensions + width), links and
// title are stored, loading is cleared and both scroll offsets are reset;
// otherwise it is ignored (a stale or non-active delivery).
// Deliver hands the widget a finished render for target: it delivers a final
// stage (loading clears). See DeliverStage. The scroll position was reset when
// the navigation to target began (startLoad), so a delivered render is shown
// from wherever the user has scrolled to — deliveries do not yank the page.
func (b *Browser) Deliver(target string, pixels []byte, imgW, imgH, width int, links []BrowserLink, title string) {
b.DeliverStage(target, pixels, imgW, imgH, width, links, title, true)
}

// DeliverStage delivers one render for target, distinguishing a final render
// from an intermediate progressive frame. When target matches the active tab's
// current URL the render (pixels + dimensions + width), links and title are
// stored; a stale or non-active delivery is ignored.
//
// With final=true the load is complete and loading clears. With final=false it
// is one staged frame of a still-running progressive render (a fast first paint,
// then refinements): the content updates but loading stays on, so the progress
// indicator keeps animating and the page does not read as "done" until the
// final frame lands. Neither form resets the scroll position — that happens once
// when the navigation begins (startLoad) — so a staged render refines in place
// instead of snapping to the top on every frame.
func (b *Browser) DeliverStage(target string, pixels []byte, imgW, imgH, width int, links []BrowserLink, title string, final bool) {
t := b.activeTab()
if t == nil || t.history[t.cursor] != target {
return
Expand All @@ -534,9 +550,9 @@ func (b *Browser) Deliver(target string, pixels []byte, imgW, imgH, width int, l
t.renderW = width
t.links = links
t.title = title
t.loading = false
t.scroll = 0
t.scrollX = 0
if final {
t.loading = false
}
b.changed()
}

Expand Down Expand Up @@ -619,6 +635,10 @@ func (b *Browser) startLoad(t *browserTab, target string) {
t.loading = true
t.hasProgress = false
t.progress = 0
// A new navigation starts at the top; deliveries afterward preserve the
// scroll, so a progressive render refines in place.
t.scroll = 0
t.scrollX = 0
t.renderW = b.renderWidth()
b.changed()
if b.OnNavigate != nil {
Expand Down
62 changes: 62 additions & 0 deletions browser_stage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2026 the go-widgets/toolkit authors. All rights reserved.
// Use of this source code is governed by a BSD-3-Clause license that can be
// found in the LICENSE file at the root of this repository.

package toolkit

import "testing"

// TestBrowserDeliverStageProgressive covers the staged-delivery contract: an
// intermediate frame updates content but keeps loading on and preserves the
// scroll position; the final frame clears loading; a new navigation resets the
// scroll.
func TestBrowserDeliverStageProgressive(t *testing.T) {
b, _, _, _ := newTestBrowser()
b.Open("http://a", "A") // startLoad: loading on, scroll reset
cr := b.contentRect()
tab := b.activeTab()
if !tab.loading {
t.Fatal("navigation should mark the tab loading")
}

tall := make([]byte, cr.W*(cr.H*3)*4)

// Intermediate frame: content lands, loading stays on.
b.DeliverStage("http://a", tall, cr.W, cr.H*3, cr.W, nil, "A", false)
if !tab.loading {
t.Error("an intermediate (final=false) frame must keep loading on")
}
if tab.imgH != cr.H*3 {
t.Error("intermediate frame content was not stored")
}

// The user scrolls during the staged load; a further intermediate frame must
// preserve that scroll (refine in place, not snap to top).
tab.scroll = 30
b.DeliverStage("http://a", tall, cr.W, cr.H*3, cr.W, nil, "A", false)
if tab.scroll != 30 {
t.Errorf("intermediate frame reset scroll to %d, want preserved 30", tab.scroll)
}

// The final frame clears loading and still preserves the scroll.
b.DeliverStage("http://a", tall, cr.W, cr.H*3, cr.W, nil, "A", true)
if tab.loading {
t.Error("a final (final=true) frame must clear loading")
}
if tab.scroll != 30 {
t.Errorf("final frame reset scroll to %d, want preserved 30", tab.scroll)
}

// Deliver (the convenience final form) also preserves scroll.
b.Deliver("http://a", tall, cr.W, cr.H*3, cr.W, nil, "A")
if tab.scroll != 30 || tab.loading {
t.Errorf("Deliver: scroll=%d loading=%v, want 30/false", tab.scroll, tab.loading)
}

// A NEW navigation resets both scroll offsets back to the top.
tab.scrollX = 12
b.Navigate("http://a/next")
if tab.scroll != 0 || tab.scrollX != 0 {
t.Errorf("navigation left scroll=%d scrollX=%d, want 0/0", tab.scroll, tab.scrollX)
}
}
Loading