Avoid flicker when changing preview

Only render the new preview when it is fully ready, keep showing
previous preview till then.
This commit is contained in:
Kovid Goyal
2025-11-22 12:24:09 +05:30
parent c5bde715a7
commit 75c3019756
3 changed files with 62 additions and 24 deletions

View File

@@ -57,6 +57,7 @@ type ImagePreview struct {
cached_data map[string]string
render_err Preview
render_channel chan render_data
ready atomic.Bool
source_img *images.ImageData
img_metadata *images.SerializableImageMetadata
renderer PreviewRenderer
@@ -65,6 +66,7 @@ type ImagePreview struct {
}
func (p *ImagePreview) IsValidForColorScheme(bool) bool { return true }
func (p *ImagePreview) IsReady() bool { return p.ready.Load() || p.render_channel == nil }
func (p *ImagePreview) Unload() {
p.source_img = nil
@@ -163,6 +165,7 @@ func (p *ImagePreview) start_rendering() {
p.render_channel <- render_data{err: parallel.Format_stacktrace_on_panic(r, 1)}
}
close(p.render_channel)
p.ready.Store(true)
p.WakeupMainThread()
}()
key, ans, err := p.disk_cache.GetPath(p.abspath)

View File

@@ -207,19 +207,20 @@ type ScreenSize struct {
}
type Handler struct {
state State
screen_size ScreenSize
result_manager *ResultManager
lp *loop.Loop
rl *readline.Readline
err_chan chan error
shortcut_tracker config.ShortcutTracker
msg_printer *message.Printer
spinner *tui.Spinner
preview_manager *PreviewManager
last_rendered_preview Preview
last_rendered_preview_abspath string
graphics_handler GraphicsHandler
state State
screen_size ScreenSize
result_manager *ResultManager
lp *loop.Loop
rl *readline.Readline
err_chan chan error
shortcut_tracker config.ShortcutTracker
msg_printer *message.Printer
spinner *tui.Spinner
preview_manager *PreviewManager
last_rendered_preview Preview
last_rendered_preview_abspath string
prev_preview_for_smooth_transition Preview
graphics_handler GraphicsHandler
}
func (self *Handler) on_escape_code(etype loop.EscapeCodeType, payload []byte) error {
@@ -838,6 +839,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
}
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
handler.init_sizes(new_size)
handler.clear_cached_previews()
return handler.draw_screen()
}
lp.OnColorSchemeChange = func(p loop.ColorPreference) (err error) {

View File

@@ -9,6 +9,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"unicode/utf8"
"github.com/kovidgoyal/go-parallel"
@@ -27,6 +28,7 @@ type Preview interface {
Render(h *Handler, x, y, width, height int)
IsValidForColorScheme(light bool) bool
Unload()
IsReady() bool
}
type PreviewManager struct {
@@ -81,6 +83,7 @@ type MessagePreview struct {
}
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
func (p MessagePreview) IsReady() bool { return true }
func (p MessagePreview) Unload() {}
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
@@ -186,14 +189,16 @@ type highlighed_data struct {
type TextFilePreview struct {
plain_text, highlighted_text string
highlighted_chan chan highlighed_data
ready atomic.Bool
light bool
path string
}
func (p TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
func (p *TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
func (p *TextFilePreview) Unload() {}
func (p *TextFilePreview) IsReady() bool { return p.ready.Load() || p.highlighted_chan == nil }
func (p *TextFilePreview) Render(h *Handler, x, y, width, height int) {
if p.highlighted_chan != nil {
select {
@@ -256,22 +261,20 @@ func (s style_resolver) TextForPath(path string) (string, error) {
return "", err
}
func (pm *PreviewManager) highlight_file_async(path string, output chan highlighed_data) {
func (pm *PreviewManager) highlight_file_async(path string, output chan highlighed_data, ready *atomic.Bool) {
s := style_resolver{light: use_light_colors, syntax_aliases: pm.settings.SyntaxAliases()}
s.light_style, s.dark_style = pm.settings.HighlightStyles()
go func() {
defer func() {
if r := recover(); r != nil {
err := parallel.Format_stacktrace_on_panic(r, 1)
debugprintln(fmt.Sprintf("Failed to highlight: %s with panic: %s", path, err))
output <- highlighed_data{err: err, light: s.light}
}
close(output)
ready.Store(true)
pm.WakeupMainThread()
}()
highlighted, err := pm.highlighter.HighlightFile(path, &s)
if err != nil {
debugprintln(fmt.Sprintf("Failed to highlight: %s with error: %s", path, err))
}
output <- highlighed_data{text: highlighted, err: err, light: s.light}
}()
}
@@ -305,8 +308,11 @@ func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Pr
const MAX_TEXT_FILE_SIZE = 16 * 1024 * 1024
if s.Size() <= MAX_TEXT_FILE_SIZE && (utils.KnownTextualMimes[mt] || strings.HasPrefix(mt, "text/")) {
ch := make(chan highlighed_data, 2)
pm.highlight_file_async(abspath, ch)
return NewTextFilePreview(abspath, s, ch, pm.highlighter.Sanitize)
ans := NewTextFilePreview(abspath, s, ch, pm.highlighter.Sanitize)
if p, ok := ans.(*TextFilePreview); ok {
pm.highlight_file_async(abspath, ch, &p.ready)
}
return ans
}
if strings.HasPrefix(mt, "image/") {
var r ImagePreviewRenderer
@@ -319,6 +325,17 @@ func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Pr
return NewFileMetadataPreview(abspath, s)
}
func (h *Handler) clear_cached_previews() {
if h.last_rendered_preview != nil {
h.last_rendered_preview.Unload()
h.last_rendered_preview = nil
}
if h.prev_preview_for_smooth_transition != nil {
h.prev_preview_for_smooth_transition.Unload()
h.prev_preview_for_smooth_transition = nil
}
}
func (h *Handler) draw_preview_content(x, y, width, height int) {
matches, _ := h.get_results()
r := matches.At(h.state.CurrentIndex())
@@ -326,13 +343,29 @@ func (h *Handler) draw_preview_content(x, y, width, height int) {
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
return
}
render := func() {
p := h.last_rendered_preview
if p.IsReady() || h.prev_preview_for_smooth_transition == nil {
p.Render(h, x, y, width, height)
if h.prev_preview_for_smooth_transition != nil {
h.prev_preview_for_smooth_transition.Unload()
h.prev_preview_for_smooth_transition = nil
}
} else {
h.prev_preview_for_smooth_transition.Render(h, x, y, width, height)
}
}
abspath := h.current_abspath()
if h.last_rendered_preview != nil {
if abspath == h.last_rendered_preview_abspath {
h.last_rendered_preview.Render(h, x, y, width, height)
render()
return
}
h.last_rendered_preview.Unload()
if h.prev_preview_for_smooth_transition != nil {
h.prev_preview_for_smooth_transition.Unload()
}
h.prev_preview_for_smooth_transition = h.last_rendered_preview
h.last_rendered_preview = nil
}
if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil {
@@ -340,6 +373,6 @@ func (h *Handler) draw_preview_content(x, y, width, height int) {
} else {
h.last_rendered_preview = p
h.last_rendered_preview_abspath = abspath
p.Render(h, x, y, width, height)
render()
}
}