~eliasnaur/gio

0b456579a96b39307724ef91203a3667a4570e3e — Chris Waldon 1 year, 4 months ago c455f0f
widget: add ReadOnly mode to editor

This commit provides a new ReadOnly boolean on the editor. If set, the
editor functions as a selectable label. User interaction cannot change
the contents of the editor (though application code can still use the
API).

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2 files changed, 106 insertions(+), 22 deletions(-)

M widget/editor.go
M widget/editor_test.go
M widget/editor.go => widget/editor.go +34 -17
@@ 37,6 37,10 @@ type Editor struct {
	// SingleLine also sets the scrolling direction to
	// horizontal.
	SingleLine bool
	// ReadOnly controls whether the contents of the editor can be altered by
	// user interaction. If set to true, the editor will allow selecting text
	// and copying it interactively, but not modifying it.
	ReadOnly bool
	// Submit enabled translation of carriage return keys to SubmitEvents.
	// If not enabled, carriage returns are inserted as newlines in the text.
	Submit bool


@@ 343,7 347,7 @@ func (e *Editor) processKey(gtx layout.Context) {
			if !e.focused || ke.State != key.Press {
				break
			}
			if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
			if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
				if !ke.Modifiers.Contain(key.ModShift) {
					e.events = append(e.events, SubmitEvent{
						Text: e.Text(),


@@ 357,6 361,9 @@ func (e *Editor) processKey(gtx layout.Context) {
		case key.SnippetEvent:
			e.updateSnippet(gtx, ke.Start, ke.End)
		case key.EditEvent:
			if e.ReadOnly {
				break
			}
			e.caret.scroll = true
			e.scroller.Stop()
			s := ke.Text


@@ 442,18 449,24 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
	}
	switch k.Name {
	case key.NameReturn, key.NameEnter:
		e.append("\n")
		if !e.ReadOnly {
			e.append("\n")
		}
	case key.NameDeleteBackward:
		if moveByWord {
			e.deleteWord(-1)
		} else {
			e.Delete(-1)
		if !e.ReadOnly {
			if moveByWord {
				e.deleteWord(-1)
			} else {
				e.Delete(-1)
			}
		}
	case key.NameDeleteForward:
		if moveByWord {
			e.deleteWord(1)
		} else {
			e.Delete(1)
		if !e.ReadOnly {
			if moveByWord {
				e.deleteWord(1)
			} else {
				e.Delete(1)
			}
		}
	case key.NameUpArrow:
		e.moveLines(-1, selAct)


@@ 488,12 501,14 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
	// Initiate a paste operation, by requesting the clipboard contents; other
	// half is in Editor.processKey() under clipboard.Event.
	case "V":
		clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
		if !e.ReadOnly {
			clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
		}
	// Copy or Cut selection -- ignored if nothing selected.
	case "C", "X":
		if text := e.SelectedText(); text != "" {
			clipboard.WriteOp{Text: text}.Add(gtx.Ops)
			if k.Name == "X" {
			if k.Name == "X" && !e.ReadOnly {
				e.Delete(1)
			}
		}


@@ 502,10 517,12 @@ func (e *Editor) command(gtx layout.Context, k key.Event) {
		e.caret.end = 0
		e.caret.start = e.Len()
	case "Z":
		if k.Modifiers.Contain(key.ModShift) {
			e.redo()
		} else {
			e.undo()
		if !e.ReadOnly {
			if k.Modifiers.Contain(key.ModShift) {
				e.redo()
			} else {
				e.undo()
			}
		}
	}
}


@@ 781,7 798,7 @@ func (e *Editor) caretWidth(gtx layout.Context) int {
}

func (e *Editor) PaintCaret(gtx layout.Context) {
	if !e.caret.on {
	if !e.caret.on || e.ReadOnly {
		return
	}
	carWidth2 := e.caretWidth(gtx)

M widget/editor_test.go => widget/editor_test.go +72 -5
@@ 92,9 92,9 @@ func assertContents(t *testing.T, e *Editor, contents string, selectionStart, se
	}
}

// TestEditorZeroDimensions ensures that an empty editor still reserves
// space for displaying its caret when the constraints allow for it.
func TestEditorZeroDimensions(t *testing.T) {
// TestEditorReadOnly ensures that mouse and keyboard interactions with readonly
// editors do nothing but manipulate the text selection.
func TestEditorReadOnly(t *testing.T) {
	gtx := layout.Context{
		Ops: new(op.Ops),
		Constraints: layout.Constraints{


@@ 102,13 102,80 @@ func TestEditorZeroDimensions(t *testing.T) {
		},
		Locale: english,
	}
	gtx.Queue = &testQueue{
		events: []event.Event{
			key.FocusEvent{Focus: true},
		},
	}
	cache := text.NewShaper(gofont.Collection())
	fontSize := unit.Sp(10)
	font := text.Font{}
	e := new(Editor)
	e.ReadOnly = true
	e.SetText("The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection. The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection.")
	cStart, cEnd := e.Selection()
	if cStart != cEnd {
		t.Errorf("unexpected initial caret positions")
	}
	dims := e.Layout(gtx, cache, font, fontSize, nil)
	if dims.Size.X < 1 || dims.Size.Y < 1 {
		t.Errorf("expected empty editor to occupy enough space to display cursor, but returned dimensions %v", dims)

	// Select everything.
	gtx.Ops.Reset()
	gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events, key.Event{Name: "A", Modifiers: key.ModShortcut})
	dims = e.Layout(gtx, cache, font, fontSize, nil)
	textContent := e.Text()
	cStart2, cEnd2 := e.Selection()
	if cStart2 > cEnd2 {
		cStart2, cEnd2 = cEnd2, cStart2
	}
	if cEnd2 != e.Len() {
		t.Errorf("expected selection to contain %d runes, got %d", e.Len(), cEnd2)
	}
	if cStart2 != 0 {
		t.Errorf("expected selection to start at rune 0, got %d", cStart2)
	}

	// Type some new characters.
	gtx.Ops.Reset()
	gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events, key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"})
	dims = e.Layout(gtx, cache, font, fontSize, nil)
	textContent2 := e.Text()
	if textContent2 != textContent {
		t.Errorf("readonly editor modified by key.EditEvent")
	}

	// Try to delete selection.
	gtx.Ops.Reset()
	gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events, key.Event{Name: key.NameDeleteBackward})
	dims = e.Layout(gtx, cache, font, fontSize, nil)
	textContent2 = e.Text()
	if textContent2 != textContent {
		t.Errorf("readonly editor modified by delete key.Event")
	}

	// Click and drag from the middle of the first line
	// to the center.
	gtx.Ops.Reset()
	gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events,
		pointer.Event{
			Type:     pointer.Press,
			Buttons:  pointer.ButtonPrimary,
			Position: f32.Pt(float32(dims.Size.X)*.5, 5),
		},
		pointer.Event{
			Type:     pointer.Drag,
			Buttons:  pointer.ButtonPrimary,
			Position: layout.FPt(dims.Size).Mul(.5),
		},
		pointer.Event{
			Type:     pointer.Release,
			Buttons:  pointer.ButtonPrimary,
			Position: layout.FPt(dims.Size).Mul(.5),
		},
	)
	cStart3, cEnd3 := e.Selection()
	if cStart3 == cStart2 || cEnd3 == cEnd2 {
		t.Errorf("expected mouse interaction to change selection.")
	}
}