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

View File

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

View File

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