More work on image support for diff

This commit is contained in:
Kovid Goyal 2023-03-25 17:42:03 +05:30
parent 9eedcc1d2a
commit cece795b16
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
4 changed files with 187 additions and 33 deletions

View file

@ -3,13 +3,17 @@
package diff
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"kitty/tools/tui/graphics"
"kitty/tools/tui/sgr"
"kitty/tools/utils"
"kitty/tools/utils/style"
"kitty/tools/wcswidth"
"strconv"
"strings"
)
var _ = fmt.Print
@ -31,10 +35,15 @@ type Reference struct {
}
type LogicalLine struct {
src Reference
line_type LineType
screen_lines []string
is_change_start bool
src Reference
line_type LineType
screen_lines []string
is_change_start bool
left_image, right_image struct {
key string
count int
}
image_lines_offset int
}
func (self *LogicalLine) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta int) {
@ -248,7 +257,8 @@ func render_diff_line(number, text, ltype string, margin_size int, available_col
return margin + content
}
func image_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) {
func image_lines(left_path, right_path string, screen_size screen_size, margin_size int, image_size graphics.Size, ans []*LogicalLine) ([]*LogicalLine, error) {
columns := screen_size.columns
available_cols := columns/2 - margin_size
ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string, formatter func(...any) string) (string, error) {
sz, err := size_for_path(path)
@ -267,6 +277,44 @@ func image_lines(left_path, right_path string, columns, margin_size int, ans []*
if err != nil {
return nil, err
}
ll.image_lines_offset = len(ll.screen_lines)
do_side := func(path string, filler string) []string {
if path == "" {
return nil
}
sz, err := image_collection.GetSizeIfAvailable(path, image_size)
if err == nil {
count := int(math.Ceil(float64(sz.Height) / float64(screen_size.cell_height)))
return utils.Repeat(filler, count)
}
if errors.Is(err, graphics.ErrNotFound) {
return style.WrapTextAsLines("Loading image...", "", available_cols)
}
return style.WrapTextAsLines(fmt.Sprintf("Failed to load image: %s", err), "", available_cols)
}
left_lines := do_side(left_path, removed_format(strings.Repeat(` `, available_cols)))
if ll.left_image.count = len(left_lines); ll.left_image.count > 0 {
ll.left_image.key = left_path
}
right_lines := do_side(right_path, added_format(strings.Repeat(` `, available_cols)))
if ll.right_image.count = len(right_lines); ll.right_image.count > 0 {
ll.right_image.key = right_path
}
filler := filler_format(strings.Repeat(` `, available_cols))
m := strings.Repeat(` `, margin_size)
get_line := func(i int, which []string, margin_fmt func(...any) string) string {
if i < len(which) {
return margin_fmt(m) + which[i]
}
return margin_filler_format(m) + filler
}
for i := 0; i < utils.Min(len(left_lines), len(right_lines)); i++ {
left, right := get_line(i, left_lines, removed_margin_format), get_line(i, right_lines, added_margin_format)
ll.screen_lines = append(ll.screen_lines, left+right)
}
ll.line_type = IMAGE_LINE
return append(ans, ll), nil
}
@ -298,7 +346,11 @@ func first_binary_line(left_path, right_path string, columns, margin_size int, r
}
line = l + r
}
ll := LogicalLine{is_change_start: true, line_type: CHANGE_LINE, src: Reference{path: left_path, linenum: 0}, screen_lines: []string{line}}
ref := left_path
if ref == "" {
ref = right_path
}
ll := LogicalLine{is_change_start: true, line_type: CHANGE_LINE, src: Reference{path: ref, linenum: 0}, screen_lines: []string{line}}
if left_path == "" {
ll.src.path = right_path
}
@ -519,20 +571,11 @@ func rename_lines(path, other_path string, columns, margin_size int, ans []*Logi
return append(ans, &ll), nil
}
func render(collection *Collection, diff_map map[string]*Patch, columns int) (result *LogicalLines, err error) {
largest_line_number := 0
collection.Apply(func(path, typ, changed_path string) error {
if typ == "diff" {
patch := diff_map[path]
if patch != nil {
largest_line_number = utils.Max(largest_line_number, patch.largest_line_number)
}
}
return nil
})
func render(collection *Collection, diff_map map[string]*Patch, screen_size screen_size, largest_line_number int, image_size graphics.Size) (result *LogicalLines, err error) {
margin_size := utils.Max(3, len(strconv.Itoa(largest_line_number))+1)
ans := make([]*LogicalLine, 0, 1024)
empty_line := LogicalLine{line_type: EMPTY_LINE}
columns := screen_size.columns
err = collection.Apply(func(path, item_type, changed_path string) error {
ans = title_lines(path, changed_path, columns, margin_size, ans)
defer func() {
@ -550,7 +593,7 @@ func render(collection *Collection, diff_map map[string]*Patch, columns int) (re
case "diff":
if is_binary {
if is_img {
ans, err = image_lines(path, changed_path, columns, margin_size, ans)
ans, err = image_lines(path, changed_path, screen_size, margin_size, image_size, ans)
} else {
ans, err = binary_lines(path, changed_path, columns, margin_size, ans)
}
@ -563,7 +606,7 @@ func render(collection *Collection, diff_map map[string]*Patch, columns int) (re
case "add":
if is_binary {
if is_img {
ans, err = image_lines("", path, columns, margin_size, ans)
ans, err = image_lines("", path, screen_size, margin_size, image_size, ans)
} else {
ans, err = binary_lines("", path, columns, margin_size, ans)
}
@ -576,7 +619,7 @@ func render(collection *Collection, diff_map map[string]*Patch, columns int) (re
case "removal":
if is_binary {
if is_img {
ans, err = image_lines(path, "", columns, margin_size, ans)
ans, err = image_lines(path, "", screen_size, margin_size, image_size, ans)
} else {
ans, err = binary_lines(path, "", columns, margin_size, ans)
}

View file

@ -26,6 +26,7 @@ const (
DIFF
HIGHLIGHT
IMAGE_LOAD
IMAGE_RESIZE
)
type ScrollPos struct {
@ -45,10 +46,12 @@ type AsyncResult struct {
rtype ResultType
collection *Collection
diff_map map[string]*Patch
page_size graphics.Size
}
var image_collection *graphics.ImageCollection
type screen_size struct{ rows, columns, num_lines, cell_width, cell_height int }
type Handler struct {
async_results chan AsyncResult
shortcut_tracker config.ShortcutTracker
@ -59,7 +62,7 @@ type Handler struct {
lp *loop.Loop
current_context_count, original_context_count int
added_count, removed_count int
screen_size struct{ rows, columns, num_lines int }
screen_size screen_size
scroll_pos, max_scroll_pos ScrollPos
restore_position *ScrollPos
inputting_command bool
@ -67,18 +70,30 @@ type Handler struct {
rl *readline.Readline
current_search *Search
current_search_is_regex, current_search_is_backward bool
largest_line_number int
images_resized_to graphics.Size
}
func (self *Handler) calculate_statistics() {
self.added_count, self.removed_count = self.collection.added_count, self.collection.removed_count
self.largest_line_number = 0
for _, patch := range self.diff_map {
self.added_count += patch.added_count
self.removed_count += patch.removed_count
self.largest_line_number = utils.Max(patch.largest_line_number, self.largest_line_number)
}
}
var DebugPrintln = tty.DebugPrintln
func (self *Handler) update_screen_size(sz loop.ScreenSize) {
self.screen_size.rows = int(sz.HeightCells)
self.screen_size.columns = int(sz.WidthCells)
self.screen_size.num_lines = self.screen_size.rows - 1
self.screen_size.cell_height = int(sz.CellHeight)
self.screen_size.cell_width = int(sz.CellWidth)
}
func (self *Handler) initialize() {
self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"})
image_collection = graphics.NewImageCollection()
@ -87,9 +102,7 @@ func (self *Handler) initialize() {
self.current_context_count = int(conf.Num_context_lines)
}
sz, _ := self.lp.ScreenSize()
self.screen_size.rows = int(sz.HeightCells)
self.screen_size.columns = int(sz.WidthCells)
self.screen_size.num_lines = self.screen_size.rows - 1
self.update_screen_size(sz)
self.original_context_count = self.current_context_count
self.lp.SetDefaultColor(loop.FOREGROUND, conf.Foreground)
self.lp.SetDefaultColor(loop.CURSOR, conf.Foreground)
@ -174,6 +187,27 @@ func (self *Handler) load_all_images() {
}()
}
func (self *Handler) resize_all_images_if_needed() {
if self.logical_lines == nil {
return
}
margin_size := self.logical_lines.margin_size
columns := self.logical_lines.columns
available_cols := columns/2 - margin_size
sz := graphics.Size{
Width: available_cols * self.screen_size.cell_width,
Height: self.screen_size.num_lines * 2 * self.screen_size.cell_height,
}
if sz != self.images_resized_to {
go func() {
image_collection.ResizeForPageSize(sz.Width, sz.Height)
r := AsyncResult{rtype: IMAGE_RESIZE, page_size: sz}
self.async_results <- r
self.lp.WakeupMainThread()
}()
}
}
func (self *Handler) rerender_diff() error {
if self.diff_map != nil && self.collection != nil {
err := self.render_diff()
@ -208,16 +242,17 @@ func (self *Handler) handle_async_result(r AsyncResult) error {
self.restore_position = nil
}
self.draw_screen()
case HIGHLIGHT, IMAGE_LOAD:
case IMAGE_RESIZE:
self.images_resized_to = r.page_size
return self.rerender_diff()
case IMAGE_LOAD, HIGHLIGHT:
return self.rerender_diff()
}
return nil
}
func (self *Handler) on_resize(old_size, new_size loop.ScreenSize) error {
self.screen_size.rows = int(new_size.HeightCells)
self.screen_size.num_lines = self.screen_size.rows - 1
self.screen_size.columns = int(new_size.WidthCells)
self.update_screen_size(new_size)
if self.diff_map != nil && self.collection != nil {
err := self.render_diff()
if err != nil {
@ -238,7 +273,7 @@ func (self *Handler) render_diff() (err error) {
if self.screen_size.rows < 2 {
return fmt.Errorf("Screen too short, need at least 2 rows")
}
self.logical_lines, err = render(self.collection, self.diff_map, self.screen_size.columns)
self.logical_lines, err = render(self.collection, self.diff_map, self.screen_size, self.largest_line_number, self.images_resized_to)
if err != nil {
return err
}
@ -259,8 +294,8 @@ func (self *Handler) render_diff() (err error) {
func (self *Handler) draw_screen() {
self.lp.StartAtomicUpdate()
defer self.lp.EndAtomicUpdate()
g := (&graphics.GraphicsCommand{}).SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_delete_visible)
g.WriteWithPayloadToLoop(self.lp, nil)
self.resize_all_images_if_needed()
image_collection.DeleteAllPlacements(self.lp)
lp.MoveCursorTo(1, 1)
lp.ClearToEndOfScreen()
if self.logical_lines == nil || self.diff_map == nil || self.collection == nil {

View file

@ -3,10 +3,12 @@
package graphics
import (
"errors"
"fmt"
"sync"
"sync/atomic"
"kitty/tools/tui/loop"
"kitty/tools/utils/images"
"golang.org/x/exp/maps"
@ -34,6 +36,24 @@ type ImageCollection struct {
images map[string]*Image
}
var ErrNotFound = errors.New("not found")
func (self *ImageCollection) GetSizeIfAvailable(key string, page_size Size) (Size, error) {
if !self.mutex.TryLock() {
return Size{}, ErrNotFound
}
defer self.mutex.Unlock()
img := self.images[key]
if img == nil {
return Size{}, ErrNotFound
}
ans := img.renderings[page_size]
if ans == nil {
return Size{}, ErrNotFound
}
return Size{ans.Width, ans.Height}, img.err
}
func (self *ImageCollection) ResolutionOf(key string) Size {
if !self.mutex.TryLock() {
return Size{-1, -1}
@ -58,6 +78,40 @@ func (self *ImageCollection) AddPaths(paths ...string) {
}
}
func (self *Image) ResizeForPageSize(width, height int) {
sz := Size{width, height}
if self.renderings[sz] != nil {
return
}
final_width, final_height := images.FitImage(self.src.size.Width, self.src.size.Height, width, height)
if final_width == self.src.size.Width && final_height == self.src.data.Height {
self.renderings[sz] = self.src.data
return
}
x_frac, y_frac := float64(final_width)/float64(self.src.size.Width), float64(final_height)/float64(self.src.size.Height)
self.renderings[sz] = self.src.data.Resize(x_frac, y_frac)
}
func (self *ImageCollection) ResizeForPageSize(width, height int) {
self.mutex.Lock()
defer self.mutex.Unlock()
ctx := images.Context{}
keys := maps.Keys(self.images)
ctx.Parallel(0, len(keys), func(nums <-chan int) {
for i := range nums {
img := self.images[keys[i]]
img.ResizeForPageSize(width, height)
}
})
}
func (self *ImageCollection) DeleteAllPlacements(lp *loop.Loop) {
g := &GraphicsCommand{}
g.SetAction(GRT_action_delete).SetDelete(GRT_delete_visible)
g.WriteWithPayloadToLoop(lp, nil)
}
func (self *ImageCollection) LoadAll() {
self.mutex.Lock()
defer self.mutex.Unlock()

View file

@ -57,6 +57,28 @@ type ImageData struct {
Frames []*ImageFrame
}
func (self *ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame {
b := self.Img.Bounds()
left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy()
ans := *self
ans.Width = int(x_frac * float64(width))
ans.Height = int(y_frac * float64(height))
ans.Img = imaging.Resize(self.Img, ans.Width, ans.Height, imaging.Lanczos)
ans.Left = int(x_frac * float64(left))
ans.Top = int(y_frac * float64(top))
return &ans
}
func (self *ImageData) Resize(x_frac, y_frac float64) *ImageData {
ans := *self
ans.Frames = utils.Map(func(f *ImageFrame) *ImageFrame { return f.Resize(x_frac, y_frac) }, self.Frames)
if len(ans.Frames) > 0 {
ans.Width, ans.Height = ans.Frames[0].Width, ans.Frames[0].Height
}
return &ans
}
func CalcMinimumGIFGap(gaps []int) int {
// Some broken GIF images have all zero gaps, browsers with their usual
// idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137