ronantakizawa/codereview-bench huggingface dataset:
Repository: fyne-io/fyne
File: widget/entry.go (Go)

Code before:
		wid := fyne.MeasureText(str, theme.TextSize(), e.TextStyle).Width
		charWid := fyne.MeasureText(string(text[i]), theme.TextSize(), e.TextStyle).Width
		if pos.X < theme.InnerPadding()+wid+(charWid/2) {
			return i
		}
	}
	return len(text)
}

func (e *Entry) cursorTextPos() (pos int) {
	return e.textPosFromRowCol(e.CursorRow, e.CursorColumn)
}

// copyToClipboard copies the current selection to a given clipboard and then removes the selected text.
// This does nothing if it is a concealed entry.
func (e *Entry) cutToClipboard(clipboard fyne.Clipboard) {
	if !e.selecting || e.Password {
		return
	}

	e.copyToClipboard(clipboard)
	e.setFieldsAndRefresh(e.eraseSelection)
	e.propertyLock.Lock()
	content := e.textProvider().String()
	if e.OnChanged != nil {
		e.OnChanged(content)
	}
	e.propertyLock.Unlock()
	e.Validate()
}

// eraseSelection removes the current selected region and moves the cursor
func (e *Entry) eraseSelection() {
	if e.Disabled() {
		return
	}

	provider := e.textProvider()
	posA, posB := e.selection()

	if posA == posB {
		return
	}

	provider.deleteFromTo(posA, posB)
	e.CursorRow, e.CursorColumn = e.rowColFromTextPos(posA)
	e.selectRow, e.selectColumn = e.CursorRow, e.CursorColumn
	e.selecting = false
	e.updateText(provider.String())
}


Review comment (performance):
I realized that this still is creating a new copy of the string. Looking at `eraseSelection`, it calls `e.updateText()` which sets the `e.Text` field and as we are in a lock we can simply reused that instead of recreating the string once more (slow and potentially uses a lot more memory for large strings).

```suggestion
	if e.OnChanged != nil {
		e.OnChanged(e.Text)
```

Code after:
		wid := fyne.MeasureText(str, theme.TextSize(), e.TextStyle).Width
		charWid := fyne.MeasureText(string(text[i]), theme.TextSize(), e.TextStyle).Width
		if pos.X < theme.InnerPadding()+wid+(charWid/2) {
			return i
		}
	}
	return len(text)
}

func (e *Entry) cursorTextPos() (pos int) {
	return e.textPosFromRowCol(e.CursorRow, e.CursorColumn)
}

// copyToClipboard copies the current selection to a given clipboard and then removes the selected text.
// This does nothing if it is a concealed entry.
func (e *Entry) cutToClipboard(clipboard fyne.Clipboard) {
	if !e.selecting || e.Password {
		return
	}

	e.copyToClipboard(clipboard)
	e.setFieldsAndRefresh(e.eraseSelection)
	e.propertyLock.Lock()
	if e.OnChanged != nil {
		e.OnChanged(e.Text)
	}
	e.propertyLock.Unlock()
	e.Validate()
}

// eraseSelection removes the current selected region and moves the cursor
func (e *Entry) eraseSelection() {
	if e.Disabled() {
		return
	}

	provider := e.textProvider()
	posA, posB := e.selection()

	if posA == posB {
		return
	}

	provider.deleteFromTo(posA, posB)
	e.CursorRow, e.CursorColumn = e.rowColFromTextPos(posA)
	e.selectRow, e.selectColumn = e.CursorRow, e.CursorColumn
	e.selecting = false
	e.updateText(provider.String())
}

func (e *Entry) getRowCol(p fyne.Position) (int, int) {
Repository: fyne-io/fyne
File: widget/entry.go (Go)

Code before:
// is either a) in progress or b) about to start
// returns true if the keypress has been fully handled
func (e *Entry) selectingKeyHandler(key *fyne.KeyEvent) bool {

	if e.selectKeyDown && !e.selecting {
		switch key.Name {
		case fyne.KeyUp, fyne.KeyDown,
			fyne.KeyLeft, fyne.KeyRight,
			fyne.KeyEnd, fyne.KeyHome,
			fyne.KeyPageUp, fyne.KeyPageDown:
			e.selecting = true
		}
	}

	if !e.selecting {
		return false
	}

	switch key.Name {
	case fyne.KeyBackspace, fyne.KeyDelete:
		// clears the selection -- return handled
		e.setFieldsAndRefresh(e.eraseSelection)
		e.propertyLock.Lock()
		content := e.textProvider().String()
		if e.OnChanged != nil {
			e.OnChanged(content)
		}
		e.propertyLock.Unlock()
		e.Validate()
		return true
	case fyne.KeyReturn, fyne.KeyEnter:
		if e.MultiLine {
			// clear the selection -- return unhandled to add the newline
			e.setFieldsAndRefresh(e.eraseSelection)
		}
		return false
	}

	if !e.selectKeyDown {
		switch key.Name {
		case fyne.KeyLeft:
			// seek to the start of the selection -- return handled
			e.propertyLock.Lock()
			selectStart, _ := e.selection()
			e.CursorRow, e.CursorColumn = e.rowColFromTextPos(selectStart)
			e.selecting = false
			e.propertyLock.Unlock()
			return true
		case fyne.KeyRight:
			// seek to the end of the selection -- return handled
			_, selectEnd := e.selection()

Review comment (suggestion):
Same comment as above applies here.

```suggestion
		if e.OnChanged != nil {
			e.OnChanged(e.Text)
```

Code after:
// returns true if the keypress has been fully handled
func (e *Entry) selectingKeyHandler(key *fyne.KeyEvent) bool {

	if e.selectKeyDown && !e.selecting {
		switch key.Name {
		case fyne.KeyUp, fyne.KeyDown,
			fyne.KeyLeft, fyne.KeyRight,
			fyne.KeyEnd, fyne.KeyHome,
			fyne.KeyPageUp, fyne.KeyPageDown:
			e.selecting = true
		}
	}

	if !e.selecting {
		return false
	}

	switch key.Name {
	case fyne.KeyBackspace, fyne.KeyDelete:
		// clears the selection -- return handled
		e.setFieldsAndRefresh(e.eraseSelection)
		e.propertyLock.Lock()
		if e.OnChanged != nil {
			e.OnChanged(e.Text)
		}
		e.propertyLock.Unlock()
		e.Validate()
		return true
	case fyne.KeyReturn, fyne.KeyEnter:
		if e.MultiLine {
			// clear the selection -- return unhandled to add the newline
			e.setFieldsAndRefresh(e.eraseSelection)
		}
		return false
	}

	if !e.selectKeyDown {
		switch key.Name {
		case fyne.KeyLeft:
			// seek to the start of the selection -- return handled
			e.propertyLock.Lock()
			selectStart, _ := e.selection()
			e.CursorRow, e.CursorColumn = e.rowColFromTextPos(selectStart)
			e.selecting = false
			e.propertyLock.Unlock()
			return true
		case fyne.KeyRight:
			// seek to the end of the selection -- return handled
			_, selectEnd := e.selection()
			e.propertyLock.Lock()
			e.CursorRow, e.CursorColumn = e.rowColFromTextPos(selectEnd)
Repository: fyne-io/fyne
File: dialog/file.go (Go)

Code before:
		f.fileName.SetText(file.Name())
		f.open.Enable()
	}
}

func (f *fileDialog) setView(view viewLayout) {
	f.view = view
	count := func() int {
		return len(f.data)
	}
	template := func() fyne.CanvasObject {
		return f.newFileItem(storage.NewFileURI("./tempfile"), true, false)
	}
	if f.view == gridView {
		f.files = widget.NewGridWrap(count, template,
			func(id widget.GridWrapItemID, o fyne.CanvasObject) {
				dir := f.data[id]
				parent := id == 0 && len(dir.Path()) < len(f.dir.Path())
				_, isDir := dir.(fyne.ListableURI)
				o.(*fileDialogItem).setLocation(dir, isDir || parent, parent)
			},
		)
		f.files.(*widget.GridWrap).OnSelected = func(id widget.GridWrapItemID) {
			f.selectedID = id
			f.setSelected(f.data[id], id)
		}
	} else {
		f.files = widget.NewList(count, template,
			func(id widget.ListItemID, o fyne.CanvasObject) {
				dir := f.data[id]
				parent := id == 0 && dir != f.dir
				_, isDir := dir.(fyne.ListableURI)
				o.(*fileDialogItem).setLocation(dir, isDir, parent)
			},
		)
		f.files.(*widget.List).OnSelected = func(id widget.ListItemID) {
			f.selectedID = id
			f.setSelected(f.data[id], id)
		}
	}
	if f.dir != nil {
		f.refreshDir(f.dir)
	}
	f.filesScroll.Content = container.NewPadded(f.files)
	f.filesScroll.Refresh()
}

// effectiveStartingDir calculates the directory at which the file dialog should
// open, based on the values of startingDirectory, CWD, home, and any error
// conditions which occur.
//

Review comment (suggestion):
You should be able to switch `widget.GridWrapItemID` to `int` and put the function next to count and template. It should make it possible to reuse the exact same function for both :)

If it is possible to use the same functions for the update functions, you should be able to do the same thing there.

Code after:
		// in a FileSave dialog
		if !f.file.save {
			f.fileName.SetText("")
			f.open.Disable()
		}
	} else {
		f.fileName.SetText(file.Name())
		f.open.Enable()
	}
}

func (f *fileDialog) setView(view viewLayout) {
	f.view = view
	count := func() int {
		return len(f.data)
	}
	template := func() fyne.CanvasObject {
		return f.newFileItem(storage.NewFileURI("./tempfile"), true, false)
	}
	update := func(id widget.GridWrapItemID, o fyne.CanvasObject) {
		dir := f.data[id]
		parent := id == 0 && len(dir.Path()) < len(f.dir.Path())
		_, isDir := dir.(fyne.ListableURI)
		o.(*fileDialogItem).setLocation(dir, isDir || parent, parent)
	}
	choose := func(id int) {
		f.selectedID = id
		f.setSelected(f.data[id], id)
	}
	if f.view == gridView {
		grid := widget.NewGridWrap(count, template, update)
		grid.OnSelected = choose
		f.files = grid
	} else {
		list := widget.NewList(count, template, update)
		list.OnSelected = choose
		f.files = list
	}
	if f.dir != nil {
		f.refreshDir(f.dir)
	}
	f.filesScroll.Content = container.NewPadded(f.files)
	f.filesScroll.Refresh()
}

// effectiveStartingDir calculates the directory at which the file dialog should
// open, based on the values of startingDirectory, CWD, home, and any error
// conditions which occur.
//
// Order of precedence is:
//
Repository: fyne-io/fyne
File: dialog/file.go (Go)

Code before:
	}

	for _, file := range files {
		if !f.showHidden && isHidden(file) {
			continue
		}

		listable, err := storage.ListerForURI(file)
		if f.file.isDirectory() && err != nil {
			continue
		} else if err == nil { // URI points to a directory
			icons = append(icons, listable)
		} else if f.file.filter == nil || f.file.filter.Matches(file) {
			icons = append(icons, file)
		}
	}
	f.data = icons

	f.files.Refresh()
	f.filesScroll.Offset = fyne.NewPos(0, 0)
	f.filesScroll.Refresh()
}

func (f *fileDialog) setLocation(dir fyne.URI) error {
	if f.selectedID > -1 {
		f.files.(interface{ Unselect(int) }).Unselect(f.selectedID)
	}
	if dir == nil {
		return fmt.Errorf("failed to open nil directory")
	}
	list, err := storage.ListerForURI(dir)
	if err != nil {
		return err
	}

	isFav := false
	for i, fav := range f.favorites {
		if fav.loc == nil {
			continue
		}
		if fav.loc.Path() == dir.Path() {
			f.favoritesList.Select(i)
			isFav = true
			break
		}
	}
	if !isFav {
		f.favoritesList.UnselectAll()
	}

	f.setSelected(nil, -1)

Review comment (refactor):
You can probably move this interface into its own type and add `.Refresh()` you use that instead of `fyne.Widget` in the fileDialog struct. It will make the code a bit tidier and avoid casting the value here.

Code after:
	if err != nil && err != repository.ErrURIRoot {
		fyne.LogError("Unable to get parent of "+dir.String(), err)
		return
	}
	if parent != nil && parent.String() != dir.String() {
		icons = append(icons, parent)
	}

	for _, file := range files {
		if !f.showHidden && isHidden(file) {
			continue
		}

		listable, err := storage.ListerForURI(file)
		if f.file.isDirectory() && err != nil {
			continue
		} else if err == nil { // URI points to a directory
			icons = append(icons, listable)
		} else if f.file.filter == nil || f.file.filter.Matches(file) {
			icons = append(icons, file)
		}
	}
	f.data = icons

	f.files.Refresh()
	f.filesScroll.Offset = fyne.NewPos(0, 0)
	f.filesScroll.Refresh()
}

func (f *fileDialog) setLocation(dir fyne.URI) error {
	if f.selectedID > -1 {
		f.files.Unselect(f.selectedID)
	}
	if dir == nil {
		return fmt.Errorf("failed to open nil directory")
	}
	list, err := storage.ListerForURI(dir)
	if err != nil {
		return err
	}

	isFav := false
	for i, fav := range f.favorites {
		if fav.loc == nil {
			continue
		}
		if fav.loc.Path() == dir.Path() {
			f.favoritesList.Select(i)
			isFav = true
			break
		}
Repository: fyne-io/fyne
File: widget/form.go (Go)

Code before:

// NewForm creates a new form widget with the specified rows of form items
// and (if any of them should be shown) a form controls row at the bottom
func NewForm(items ...*FormItem) *Form {
	form := &Form{Items: items}
	form.ExtendBaseWidget(form)

	return form
}

type formItemLayout struct{}

func (f formItemLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) {
	itemHeight := objs[0].MinSize().Height
	objs[0].Resize(fyne.NewSize(size.Width, itemHeight))

	objs[1].Move(fyne.NewPos(theme.InnerPadding(), itemHeight+theme.InnerPadding()/2))
	objs[1].Resize(fyne.NewSize(size.Width, objs[1].MinSize().Width))
}

func (f formItemLayout) MinSize(objs []fyne.CanvasObject) fyne.Size {
	min0 := objs[0].MinSize()
	min1 := objs[1].MinSize()

	minWidth := fyne.Max(min0.Width, min1.Width)
	return fyne.NewSize(minWidth, min0.Height+min1.Height).AddWidthHeight(0, theme.InnerPadding())
}


Review comment (suggestion):
No need for the extra add here. You can move it into the size creation.
```suggestion
	return fyne.NewSize(minWidth, min0.Height+min1.Height+theme.InnerPadding())
```

Code after:

// NewForm creates a new form widget with the specified rows of form items
// and (if any of them should be shown) a form controls row at the bottom
func NewForm(items ...*FormItem) *Form {
	form := &Form{Items: items}
	form.ExtendBaseWidget(form)

	return form
}

type formItemLayout struct{}

func (f formItemLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) {
	itemHeight := objs[0].MinSize().Height
	objs[0].Resize(fyne.NewSize(size.Width, itemHeight))

	objs[1].Move(fyne.NewPos(theme.InnerPadding(), itemHeight+theme.InnerPadding()/2))
	objs[1].Resize(fyne.NewSize(size.Width, objs[1].MinSize().Width))
}

func (f formItemLayout) MinSize(objs []fyne.CanvasObject) fyne.Size {
	min0 := objs[0].MinSize()
	min1 := objs[1].MinSize()

	minWidth := fyne.Max(min0.Width, min1.Width)
	return fyne.NewSize(minWidth, min0.Height+min1.Height+theme.InnerPadding())
}
Repository: fyne-io/fyne
File: widget/accordion_test.go (Go)

Code before:
			items: []*widget.AccordionItem{
				{
					Title:  "A",
					Detail: widget.NewLabel("11111"),
				},
				{
					Title:  "B",
					Detail: widget.NewLabel("2222222222"),
				},
			},
			opened: []int{0, 1},
		},
	} {
		t.Run(name, func(t *testing.T) {
			accordion := &widget.Accordion{
				MultiOpen: tt.multiOpen,
			}
			for _, ai := range tt.items {
				accordion.Append(ai)
			}
			for _, o := range tt.opened {
				accordion.Open(o)
			}

			window := test.NewWindow(&fyne.Container{Layout: layout.NewCenterLayout(), Objects: []fyne.CanvasObject{accordion}})
			window.Resize(accordion.MinSize().Add(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)))

			test.AssertRendersToMarkup(t, "accordion/layout_"+name+".xml", window.Canvas())

			window.Close()
		})
	}
}

func TestAccordion_Layout_Expanded(t *testing.T) {
	test.NewApp()

	for name, tt := range map[string]struct {
		multiOpen bool
		items     []*widget.AccordionItem
		opened    []int
	}{
		"expanded_single_open_one_item": {
			items: []*widget.AccordionItem{
				{
					Title:  "A",
					Detail: widget.NewLabel("11111"),
				},
			},
		},
		"expanded_single_open_one_item_opened": {

Review comment (suggestion):
```suggestion
			window.Resize(accordion.MinSize().Add(fyne.NewSquareSize(theme.Padding()*2)))
```

Code after:
			items: []*widget.AccordionItem{
				{
					Title:  "A",
					Detail: widget.NewLabel("11111"),
				},
				{
					Title:  "B",
					Detail: widget.NewLabel("2222222222"),
				},
			},
			opened: []int{0, 1},
		},
	} {
		t.Run(name, func(t *testing.T) {
			accordion := &widget.Accordion{
				MultiOpen: tt.multiOpen,
			}
			for _, ai := range tt.items {
				accordion.Append(ai)
			}
			for _, o := range tt.opened {
				accordion.Open(o)
			}

			window := test.NewWindow(&fyne.Container{Layout: layout.NewCenterLayout(), Objects: []fyne.CanvasObject{accordion}})
			window.Resize(accordion.MinSize().Add(fyne.NewSquareSize(theme.Padding() * 2)))

			test.AssertRendersToMarkup(t, "accordion/layout_"+name+".xml", window.Canvas())

			window.Close()
		})
	}
}

func TestAccordion_Layout_Expanded(t *testing.T) {
	test.NewApp()

	for name, tt := range map[string]struct {
		multiOpen bool
		items     []*widget.AccordionItem
		opened    []int
	}{
		"expanded_single_open_one_item": {
			items: []*widget.AccordionItem{
				{
					Title:  "A",
					Detail: widget.NewLabel("11111"),
				},
			},
		},
		"expanded_single_open_one_item_opened": {
Repository: fyne-io/fyne
File: widget/richtext_objects.go (Go)

Code before:
	return r.min
}

func (r *richImage) setAlign(a fyne.TextAlign) {
	if r.layout != nil {
		r.layout.Refresh()
	}
	r.align = a
}

type richImageLayout struct {
	r *richImage
}

func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
	r.r.img.Resize(r.r.min)
	gap := s.Width - r.r.min.Width

	switch r.r.align {
	case fyne.TextAlignCenter:
		r.r.img.Move(fyne.NewPos(gap/2, 0))
	case fyne.TextAlignTrailing:
		r.r.img.Move(fyne.NewPos(gap, 0))
	default:
		r.r.img.Move(fyne.NewPos(0, 0))
	}
}

func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
	return r.r.min
}

type unpadTextWidgetLayout struct {
}

func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
	pad := theme.InnerPadding() * -1
	pad2 := pad * -2

	o[0].Move(fyne.NewPos(pad, pad))
	o[0].Resize(s.Add(fyne.NewSize(pad2, pad2)))
}

func (u *unpadTextWidgetLayout) MinSize(o []fyne.CanvasObject) fyne.Size {
	pad := theme.InnerPadding() * 2
	return o[0].MinSize().Subtract(fyne.NewSize(pad, pad))
}


Review comment (suggestion):
Just a small suggestion:
```go
	gap := float32(0)

	switch r.r.align {
	case fyne.TextAlignCenter:
		gap = (s.Width - r.r.min.Width) / 2
	case fyne.TextAlignTrailing:
		gap = s.Width - r.r.min.Width
	}

	r.r.img.Move(fyne.NewPos(gap, 0))
```

Code after:
	return r.min
}

func (r *richImage) setAlign(a fyne.TextAlign) {
	if r.layout != nil {
		r.layout.Refresh()
	}
	r.align = a
}

type richImageLayout struct {
	r *richImage
}

func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
	r.r.img.Resize(r.r.min)
	gap := float32(0)

	switch r.r.align {
	case fyne.TextAlignCenter:
		gap = (s.Width - r.r.min.Width) / 2
	case fyne.TextAlignTrailing:
		gap = s.Width - r.r.min.Width
	}

	r.r.img.Move(fyne.NewPos(gap, 0))
}

func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
	return r.r.min
}

type unpadTextWidgetLayout struct {
}

func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) {
	pad := theme.InnerPadding() * -1
	pad2 := pad * -2

	o[0].Move(fyne.NewPos(pad, pad))
	o[0].Resize(s.Add(fyne.NewSize(pad2, pad2)))
}

func (u *unpadTextWidgetLayout) MinSize(o []fyne.CanvasObject) fyne.Size {
	pad := theme.InnerPadding() * 2
	return o[0].MinSize().Subtract(fyne.NewSize(pad, pad))
}
Repository: fyne-io/fyne
File: widget/entry.go (Go)

Code before:
		str := string(text[0:i])
		wid := fyne.MeasureText(str, theme.TextSize(), e.TextStyle).Width
		charWid := fyne.MeasureText(string(text[i]), theme.TextSize(), e.TextStyle).Width
		if pos.X < theme.InnerPadding()+wid+(charWid/2) {
			return i
		}
	}
	return len(text)
}

func (e *Entry) cursorTextPos() (pos int) {
	return e.textPosFromRowCol(e.CursorRow, e.CursorColumn)
}

// copyToClipboard copies the current selection to a given clipboard and then removes the selected text.
// This does nothing if it is a concealed entry.
func (e *Entry) cutToClipboard(clipboard fyne.Clipboard) {
	if !e.selecting || e.Password {
		return
	}

	e.copyToClipboard(clipboard)
	e.setFieldsAndRefresh(e.eraseSelection)
	e.propertyLock.Lock()
	cb := e.OnChanged
	e.propertyLock.Unlock()
	if cb != nil {
		cb(e.Text)
	}
	e.Validate()
}

// eraseSelection removes the current selected region and moves the cursor
func (e *Entry) eraseSelection() {
	if e.Disabled() {
		return
	}

	provider := e.textProvider()
	posA, posB := e.selection()

	if posA == posB {
		return
	}

	provider.deleteFromTo(posA, posB)
	e.CursorRow, e.CursorColumn = e.rowColFromTextPos(posA)
	e.selectRow, e.selectColumn = e.CursorRow, e.CursorColumn
	e.selecting = false
	e.updateText(provider.String())
}

Review comment (suggestion):
You are only reading the value here, so you can use `RLock()` and `RUnlock()` to allow other readers to not be blocked. 

Code after:
		str := string(text[0:i])
		wid := fyne.MeasureText(str, theme.TextSize(), e.TextStyle).Width
		charWid := fyne.MeasureText(string(text[i]), theme.TextSize(), e.TextStyle).Width
		if pos.X < theme.InnerPadding()+wid+(charWid/2) {
			return i
		}
	}
	return len(text)
}

func (e *Entry) cursorTextPos() (pos int) {
	return e.textPosFromRowCol(e.CursorRow, e.CursorColumn)
}

// copyToClipboard copies the current selection to a given clipboard and then removes the selected text.
// This does nothing if it is a concealed entry.
func (e *Entry) cutToClipboard(clipboard fyne.Clipboard) {
	if !e.selecting || e.Password {
		return
	}

	e.copyToClipboard(clipboard)
	e.setFieldsAndRefresh(e.eraseSelection)
	e.propertyLock.RLock()
	cb := e.OnChanged
	e.propertyLock.RUnlock()
	if cb != nil {
		cb(e.Text)
	}
	e.Validate()
}

// eraseSelection removes the current selected region and moves the cursor
func (e *Entry) eraseSelection() {
	if e.Disabled() {
		return
	}

	provider := e.textProvider()
	posA, posB := e.selection()

	if posA == posB {
		return
	}

	provider.deleteFromTo(posA, posB)
	e.CursorRow, e.CursorColumn = e.rowColFromTextPos(posA)
	e.selectRow, e.selectColumn = e.CursorRow, e.CursorColumn
	e.selecting = false
	e.updateText(provider.String())
}
Repository: fyne-io/fyne
File: widget/textgrid.go (Go)

Code before:
// NewTextGrid creates a new empty TextGrid widget.
func NewTextGrid() *TextGrid {
	grid := &TextGrid{}
	grid.ExtendBaseWidget(grid)
	return grid
}

// NewTextGridFromString creates a new TextGrid widget with the specified string content.
func NewTextGridFromString(content string) *TextGrid {
	grid := NewTextGrid()
	grid.SetText(content)
	return grid
}

// nextTab finds the column of the next tab stop for the given column
func nextTab(column int, tabWidth int) int {
	tabStop, _ := math.Modf(float64(column+tabWidth) / float64(tabWidth))
	return tabWidth * int(tabStop)
}

type textGridRenderer struct {
	text *TextGrid

	cols, rows int

	cellSize fyne.Size
	objects  []fyne.CanvasObject
	current  fyne.Canvas
}

func (t *textGridRenderer) appendTextCell(str rune) {
	text := canvas.NewText(string(str), theme.ForegroundColor())
	text.TextStyle.Monospace = true

	bg := canvas.NewRectangle(color.Transparent)

	ul := canvas.NewLine(color.Transparent)

	t.objects = append(t.objects, bg, text, ul)
}

func (t *textGridRenderer) refreshCell(row, col int) {
	pos := row*t.cols + col
	if pos*3+1 >= len(t.objects) {
		return
	}

	cell := t.text.Rows[row].Cells[col]
	t.setCellRune(cell.Rune, pos, cell.Style, t.text.Rows[row].Style)
}


Review comment (performance):
Won't having 50% more objects always visible slow things down?
If adding them cannot be avoided at least avoid showing them... or maybe we need an optimisation in line drawing to ignore transparent lines? (I don't think that exists)...

Code after:
// nextTab finds the column of the next tab stop for the given column
func nextTab(column int, tabWidth int) int {
	tabStop, _ := math.Modf(float64(column+tabWidth) / float64(tabWidth))
	return tabWidth * int(tabStop)
}

type textGridRenderer struct {
	text *TextGrid

	cols, rows int

	cellSize fyne.Size
	objects  []fyne.CanvasObject
	current  fyne.Canvas
}

func (t *textGridRenderer) appendTextCell(str rune) {
	th := t.text.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()

	text := canvas.NewText(string(str), th.Color(theme.ColorNameForeground, v))
	text.TextStyle.Monospace = true

	bg := canvas.NewRectangle(color.Transparent)

	ul := canvas.NewLine(color.Transparent)

	t.objects = append(t.objects, bg, text, ul)
}

func (t *textGridRenderer) refreshCell(row, col int) {
	pos := row*t.cols + col
	if pos*3+1 >= len(t.objects) {
		return
	}

	cell := t.text.Rows[row].Cells[col]
	t.setCellRune(cell.Rune, pos, cell.Style, t.text.Rows[row].Style)
}

func (t *textGridRenderer) setCellRune(str rune, pos int, style, rowStyle TextGridStyle) {
	if str == 0 {
		str = ' '
	}
	rect := t.objects[pos*3].(*canvas.Rectangle)
	text := t.objects[pos*3+1].(*canvas.Text)
	underline := t.objects[pos*3+2].(*canvas.Line)

	th := t.text.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()
	fg := th.Color(theme.ColorNameForeground, v)