diff --git a/examples/main.go b/examples/main.go index 194bfb6fc0..b4ebb05749 100644 --- a/examples/main.go +++ b/examples/main.go @@ -67,7 +67,7 @@ func welcome(app fyne.App) { W.NewGroup("Demos", []fyne.CanvasObject{ appButton(app, "Canvas", canvasApp), appButton(app, "Layout", layoutApp), - &W.Entry{}, + &W.Entry{Text: "Entry"}, &W.Check{Text: "Check", OnChanged: func(on bool) { fmt.Println("checked", on) }}, }...), diff --git a/widget/entry.go b/widget/entry.go index 33ca91edde..37d0f1c5dd 100644 --- a/widget/entry.go +++ b/widget/entry.go @@ -1,25 +1,30 @@ package widget import "fmt" +import "log" import "github.com/fyne-io/fyne" import "github.com/fyne-io/fyne/canvas" import "github.com/fyne-io/fyne/theme" type entryRenderer struct { - label *canvas.Text + label *Label bg, box *canvas.Rectangle objects []fyne.CanvasObject entry *Entry } +func emptyTextMinSize(style fyne.TextStyle) fyne.Size { + return fyne.GetDriver().RenderedTextSize("M", theme.TextSize(), style) +} + // MinSize calculates the minimum size of an entry widget. // This is based on the contained text with a standard amount of padding added. func (e *entryRenderer) MinSize() fyne.Size { var textSize fyne.Size if e.label.Text == "" { - textSize = fyne.GetDriver().RenderedTextSize("M", e.label.TextSize, e.label.TextStyle) + textSize = emptyTextMinSize(e.label.TextStyle) } else { textSize = e.label.MinSize() } @@ -40,13 +45,13 @@ func (e *entryRenderer) Layout(size fyne.Size) { // ApplyTheme is called when the Entry may need to update it's look. func (e *entryRenderer) ApplyTheme() { - e.label.Color = theme.TextColor() + e.label.ApplyTheme() e.box.FillColor = theme.BackgroundColor() e.Refresh() } func (e *entryRenderer) Refresh() { - e.label.Text = e.entry.Text + e.label.SetText(e.entry.Text) if e.entry.focused { e.bg.FillColor = theme.FocusColor() @@ -61,7 +66,7 @@ func (e *entryRenderer) Objects() []fyne.CanvasObject { return e.objects } -// Entry widget allows simple text to be input when focussed. +// Entry widget allows simple text to be input when focused. type Entry struct { baseWidget @@ -101,7 +106,7 @@ func (e *Entry) Focused() bool { return e.focused } -// OnKeyDown receives key input events when the Entry widget is focussed. +// OnKeyDown receives key input events when the Entry widget is focused. func (e *Entry) OnKeyDown(key *fyne.KeyEvent) { if key.Name == "BackSpace" { if len(e.Text) == 0 { @@ -112,16 +117,17 @@ func (e *Entry) OnKeyDown(key *fyne.KeyEvent) { substr := string(runes[0 : len(runes)-1]) e.SetText(substr) - return - } - - if key.String != "" { + } else if key.Name == "Return" { + e.SetText(fmt.Sprintf("%s\n", e.Text)) + } else if key.String != "" { e.SetText(fmt.Sprintf("%s%s", e.Text, key.String)) + } else { + log.Println("Unhandled key press", key.String) } } func (e *Entry) createRenderer() fyne.WidgetRenderer { - text := canvas.NewText(e.Text, theme.TextColor()) + text := NewLabel(e.Text) bg := canvas.NewRectangle(theme.ButtonColor()) box := canvas.NewRectangle(theme.BackgroundColor()) diff --git a/widget/label.go b/widget/label.go index fdb9d0faf8..8ac49bcf79 100644 --- a/widget/label.go +++ b/widget/label.go @@ -1,5 +1,8 @@ package widget +import "bufio" +import "strings" + import "github.com/fyne-io/fyne" import "github.com/fyne-io/fyne/canvas" import "github.com/fyne-io/fyne/theme" @@ -8,19 +11,88 @@ type labelRenderer struct { objects []fyne.CanvasObject background *canvas.Rectangle - text *canvas.Text + texts []*canvas.Text label *Label + lines int +} + +func (l *labelRenderer) parseText(text string) []string { + if !strings.Contains(text, "\n") { + return []string{text} + } + + var texts []string + s := bufio.NewScanner(strings.NewReader(text)) + for s.Scan() { + texts = append(texts, s.Text()) + } + // this checks if Scan() ended on a blank line + if string(text[len(text)-1]) == "\n" { + texts = append(texts, "") + } + + return texts +} + +func (l *labelRenderer) updateTexts(strings []string) { + l.lines = len(strings) + count := len(l.texts) + refresh := false + + for i, str := range strings { + if i >= count { + text := canvas.NewText("", theme.TextColor()) + l.texts = append(l.texts, text) + l.objects = append(l.objects, text) + + refresh = true + } + l.texts[i].Text = str + } + + for i := l.lines; i < len(l.texts); i++ { + l.texts[i].Text = "" + refresh = true + } + + if refresh { + // TODO invalidate container size (to shrink) + l.Refresh() + } } // MinSize calculates the minimum size of a label. // This is based on the contained text with a standard amount of padding added. func (l *labelRenderer) MinSize() fyne.Size { - return l.text.MinSize().Add(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)) + height := 0 + width := 0 + for i := 0; i < l.lines; i++ { + min := l.texts[i].MinSize() + if l.texts[i].Text == "" { + min = emptyTextMinSize(l.label.TextStyle) + } + height += min.Height + width = fyne.Max(width, min.Width) + } + + return fyne.NewSize(width, height).Add(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)) } func (l *labelRenderer) Layout(size fyne.Size) { - l.text.Resize(size) + yPos := theme.Padding() + lineHeight := size.Height - theme.Padding()*2 + if len(l.texts) > 1 { + lineHeight = lineHeight / l.lines + } + lineSize := fyne.NewSize(size.Width, lineHeight) + for i := 0; i < l.lines; i++ { + text := l.texts[i] + text.Resize(lineSize) + text.Move(fyne.NewPos(theme.Padding(), yPos)) + yPos += lineHeight + } + l.background.Resize(size) } @@ -32,13 +104,16 @@ func (l *labelRenderer) Objects() []fyne.CanvasObject { func (l *labelRenderer) ApplyTheme() { l.background.FillColor = theme.BackgroundColor() - l.text.Color = theme.TextColor() + for _, text := range l.texts { + text.Color = theme.TextColor() + } } func (l *labelRenderer) Refresh() { - l.text.Alignment = l.label.Alignment - l.text.TextStyle = l.label.TextStyle - l.text.Text = l.label.Text + for _, text := range l.texts { + text.Alignment = l.label.Alignment + text.TextStyle = l.label.TextStyle + } fyne.RefreshObject(l.label) } @@ -56,21 +131,25 @@ type Label struct { func (l *Label) SetText(text string) { l.Text = text + render := l.Renderer().(*labelRenderer) + for _, obj := range render.texts { + obj.Text = "" + } + + render.updateTexts(render.parseText(l.Text)) + l.Renderer().Refresh() } func (l *Label) createRenderer() fyne.WidgetRenderer { - obj := canvas.NewText(l.Text, theme.TextColor()) - obj.Alignment = l.Alignment - obj.TextStyle = l.TextStyle - bg := canvas.NewRectangle(theme.ButtonColor()) - - objects := []fyne.CanvasObject{ - bg, - obj, - } + render := &labelRenderer{label: l} + + render.background = canvas.NewRectangle(theme.ButtonColor()) + render.texts = []*canvas.Text{} + render.objects = []fyne.CanvasObject{render.background} + render.updateTexts(render.parseText(l.Text)) - return &labelRenderer{objects, bg, obj, l} + return render } // Renderer is a private method to Fyne which links this widget to it's renderer diff --git a/widget/label_test.go b/widget/label_test.go index bea3623a8e..4fa270432d 100644 --- a/widget/label_test.go +++ b/widget/label_test.go @@ -8,7 +8,7 @@ import "github.com/fyne-io/fyne" import _ "github.com/fyne-io/fyne/test" import "github.com/fyne-io/fyne/theme" -func TestLabelSize(t *testing.T) { +func TestLabel_MinSize(t *testing.T) { label := NewLabel("Test") min := label.MinSize() @@ -21,5 +21,22 @@ func TestLabelSize(t *testing.T) { func TestLabel_Alignment(t *testing.T) { label := &Label{Text: "Test", Alignment: fyne.TextAlignTrailing} - assert.Equal(t, fyne.TextAlignTrailing, label.Renderer().(*labelRenderer).text.Alignment) + assert.Equal(t, fyne.TextAlignTrailing, label.Renderer().(*labelRenderer).texts[0].Alignment) +} + +func TestText_MinSize_Multiline(t *testing.T) { + text := NewLabel("Break") + min := text.MinSize() + + text = NewLabel("Bre\nak") + min2 := text.MinSize() + assert.True(t, min2.Width < min.Width) + assert.True(t, min2.Height > min.Height) + + yPos := -1 + for _, text := range text.Renderer().(*labelRenderer).texts { + assert.True(t, text.CurrentSize().Height < min2.Height) + assert.True(t, text.CurrentPosition().Y > yPos) + yPos = text.CurrentPosition().Y + } }