Skip to content

Commit f97ebb9

Browse files
committed
Add support for canvas image type
1 parent 7d3f9f2 commit f97ebb9

File tree

3 files changed

+206
-2
lines changed

3 files changed

+206
-2
lines changed

internal/guidefs/graphics.go

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package guidefs
22

33
import (
4+
"errors"
45
"fmt"
56
"image/color"
7+
"path/filepath"
68
"strconv"
79

810
"fyne.io/fyne/v2"
@@ -53,6 +55,7 @@ func initGraphics() {
5355
return []string{"canvas", "image/color"}
5456
},
5557
},
58+
"*canvas.Image": initImageGraphic(),
5659
"*canvas.LinearGradient": {
5760
Name: "LinearGradient",
5861
Create: func(Context) fyne.CanvasObject {
@@ -206,7 +209,165 @@ func initGraphics() {
206209
GraphicsNames = extractNames(Graphics)
207210
}
208211

209-
// TODO tidy the API and move to a widget package
212+
func initImageGraphic() WidgetInfo {
213+
return WidgetInfo{
214+
Name: "Image",
215+
Create: func(Context) fyne.CanvasObject {
216+
return &canvas.Image{}
217+
},
218+
Edit: func(obj fyne.CanvasObject, c Context, _ func([]*widget.FormItem), onchanged func()) []*widget.FormItem {
219+
i := obj.(*canvas.Image)
220+
props := c.Metadata()[obj]
221+
222+
minWidthInput := widget.NewEntry()
223+
minWidthInput.SetText(props["minWidth"])
224+
minHeightInput := widget.NewEntry()
225+
minHeightInput.SetText(props["minHeight"])
226+
227+
minWidthInput.Validator = func(s string) error {
228+
if s == "" {
229+
return nil
230+
}
231+
232+
f, err := strconv.ParseFloat(s, 32)
233+
if err != nil {
234+
return errors.New("invalid number format")
235+
}
236+
if f < 0 {
237+
return errors.New("negative minimum size")
238+
}
239+
return nil
240+
}
241+
minHeightInput.Validator = minWidthInput.Validator
242+
243+
updateMin := func(_ string) {
244+
w, err := strconv.ParseFloat(minWidthInput.Text, 32)
245+
if err != nil {
246+
props["minWidth"] = ""
247+
return
248+
} else {
249+
props["minWidth"] = minWidthInput.Text
250+
}
251+
h, err := strconv.ParseFloat(minHeightInput.Text, 32)
252+
if err != nil {
253+
props["minHeight"] = ""
254+
return
255+
} else {
256+
props["minHeight"] = minHeightInput.Text
257+
}
258+
259+
s := fyne.Size{}
260+
if w > 0 || h > 0 {
261+
s = fyne.NewSize(float32(w), float32(h))
262+
}
263+
i.SetMinSize(s)
264+
onchanged()
265+
}
266+
minWidthInput.OnChanged = updateMin
267+
minHeightInput.OnChanged = updateMin
268+
269+
fill := widget.NewSelect([]string{"Stretch", "Contain", "Original size"}, func(s string) {
270+
mode := canvas.ImageFillStretch
271+
switch s {
272+
case "Contain":
273+
mode = canvas.ImageFillContain
274+
updateMin("") // reset possible original fill override
275+
case "Original size":
276+
mode = canvas.ImageFillOriginal
277+
default:
278+
updateMin("") // reset possible original fill override
279+
}
280+
i.FillMode = mode
281+
i.Refresh()
282+
onchanged()
283+
})
284+
fill.SetSelectedIndex(int(i.FillMode))
285+
286+
// TODO move to go:embed for files!
287+
pathSelect := widget.NewButton("(No file)", nil)
288+
if i.File != "" {
289+
pathSelect.SetText(filepath.Base(i.File))
290+
}
291+
pathSelect.OnTapped = func() {
292+
dialog.ShowFileOpen(func(r fyne.URIReadCloser, err error) {
293+
path := ""
294+
if r != nil && err == nil {
295+
_ = r.Close()
296+
path = r.URI().Path()
297+
}
298+
299+
i.File = path
300+
if path == "" {
301+
i.Image = nil
302+
pathSelect.SetText("(No file)")
303+
} else {
304+
pathSelect.SetText(r.URI().Name())
305+
}
306+
i.Refresh()
307+
onchanged()
308+
}, fyne.CurrentApp().Driver().AllWindows()[0])
309+
}
310+
311+
resSelect := newIconSelectorButton(i.Resource, func(res fyne.Resource) {
312+
i.Resource = res
313+
i.Refresh()
314+
onchanged()
315+
}, true)
316+
resSelect.SetIcon(i.Resource)
317+
318+
return []*widget.FormItem{
319+
widget.NewFormItem("Min Width", minWidthInput),
320+
widget.NewFormItem("Min Height", minHeightInput),
321+
widget.NewFormItem("Fill mode", fill),
322+
widget.NewFormItem("Path", pathSelect),
323+
widget.NewFormItem("Resource", resSelect),
324+
}
325+
},
326+
Packages: func(obj fyne.CanvasObject, _ Context) []string {
327+
i := obj.(*canvas.Image)
328+
if i.Resource != nil {
329+
return []string{"canvas", "theme"}
330+
}
331+
return []string{"canvas"}
332+
},
333+
Gostring: func(obj fyne.CanvasObject, c Context, defs map[string]string) string {
334+
i := obj.(*canvas.Image)
335+
props := c.Metadata()[obj]
336+
minWidth, _ := props["minWidth"]
337+
minHeight, _ := props["minHeight"]
338+
hasMin := (minWidth != "" && minWidth != "0") || (minHeight != "" && minHeight != "0")
339+
340+
code := ""
341+
if i.Resource != nil {
342+
res := "theme." + IconName(i.Resource) + "()"
343+
344+
if !hasMin && i.FillMode == canvas.ImageFillStretch {
345+
code = fmt.Sprintf("canvas.NewImageFromResource(%s)", res)
346+
} else {
347+
code = fmt.Sprintf("&canvas.Image{Resource: %s, FillMode: %s}", res, fillName(i.FillMode))
348+
349+
if hasMin {
350+
code = fmt.Sprintf("func() *canvas.Image {"+
351+
"img := %s; img.SetMinSize(%#v); return img}()", code, i.MinSize())
352+
}
353+
}
354+
} else {
355+
if !hasMin && i.FillMode == canvas.ImageFillStretch {
356+
code = fmt.Sprintf("canvas.NewImageFromFile(\"%s\")", i.File)
357+
} else {
358+
code = fmt.Sprintf("&canvas.Image{File: \"%s\", FillMode: %s}", i.File, fillName(i.FillMode))
359+
360+
if hasMin {
361+
code = fmt.Sprintf("func() *canvas.Image {"+
362+
"img := %s; img.SetMinSize(%#v); return img}()", code, i.MinSize())
363+
}
364+
}
365+
}
366+
367+
return widgetRef(props, defs, code)
368+
},
369+
}
370+
}
210371

211372
func newColorButton(c color.Color, fn func(color.Color)) fyne.CanvasObject {
212373
// TODO get the window passed in somehow

internal/guidefs/utils.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"strings"
55

66
"fyne.io/fyne/v2"
7+
"fyne.io/fyne/v2/canvas"
78
"fyne.io/fyne/v2/container"
89
"fyne.io/fyne/v2/widget"
910
)
@@ -21,6 +22,17 @@ func directionName(d container.ScrollDirection) string {
2122
}
2223
}
2324

25+
func fillName(f canvas.ImageFill) string {
26+
switch f {
27+
case canvas.ImageFillContain:
28+
return "canvas.ImageFillContain"
29+
case canvas.ImageFillOriginal:
30+
return "canvas.ImageFillOriginal"
31+
default:
32+
return "canvas.ImageFillStretch"
33+
}
34+
}
35+
2436
func escapeLabel(inStr string) (outStr string) {
2537
outStr = strings.ReplaceAll(inStr, "\"", "\\\"")
2638
outStr = strings.ReplaceAll(outStr, "\n", "\\n")

json.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import (
88
"log"
99
"net/url"
1010
"reflect"
11+
"strconv"
1112
"strings"
1213
"time"
1314

1415
"fyne.io/fyne/v2"
16+
"fyne.io/fyne/v2/canvas"
1517
"fyne.io/fyne/v2/container"
1618
"fyne.io/fyne/v2/theme"
1719
"fyne.io/fyne/v2/widget"
@@ -232,6 +234,17 @@ func DecodeMap(m map[string]interface{}, d Context) (fyne.CanvasObject, error) {
232234
props["name"] = name.(string)
233235
}
234236

237+
if setMin, ok := obj.(interface{ SetMinSize(fyne.Size) }); ok {
238+
minWithStr, _ := props["minWidth"]
239+
minHeightStr, _ := props["minHeight"]
240+
if (minWithStr != "" && minWithStr != "0") || (minHeightStr != "" && minHeightStr != "0") {
241+
minWidth, _ := strconv.ParseFloat(minWithStr, 64)
242+
minHeight, _ := strconv.ParseFloat(minHeightStr, 64)
243+
244+
setMin.SetMinSize(fyne.NewSize(float32(minWidth), float32(minHeight)))
245+
}
246+
}
247+
235248
if set, ok := m["Actions"]; ok {
236249
if actions, ok := set.(map[string]any); ok {
237250
for k, v := range actions {
@@ -277,6 +290,23 @@ func EncodeMap(obj fyne.CanvasObject, d Context) (interface{}, error) {
277290
}
278291

279292
switch c := obj.(type) {
293+
case *canvas.Image:
294+
img := c.Image
295+
res := c.Resource
296+
go func() { // TODO find a better way to reset this after encoding
297+
time.Sleep(time.Millisecond * 100)
298+
c.Image = img
299+
c.Resource = res
300+
}()
301+
302+
c.Image = nil
303+
if c.Resource == nil {
304+
return encodeWidget(c, name, actions, props), nil
305+
}
306+
307+
c.Resource = guidefs.WrapResource(c.Resource)
308+
wid := encodeWidget(c, name, actions, props)
309+
return wid, nil
280310
case *widget.Accordion:
281311
node := &cntObj{Struct: make(map[string]interface{})}
282312
node.Type = "*widget.Accordion"
@@ -580,7 +610,8 @@ func decodeFields(e reflect.Value, in map[string]interface{}, d Context) error {
580610
typeName := f.Type().String()
581611
switch typeName {
582612
case "fyne.TextAlign", "fyne.TextTruncation", "fyne.TextWrap", "widget.ButtonAlign", "widget.ButtonImportance",
583-
"widget.ButtonIconPlacement", "widget.Importance", "widget.Orientation", "widget.ScrollDirection", "fyne.ScrollDirection":
613+
"widget.ButtonIconPlacement", "widget.Importance", "widget.Orientation", "widget.ScrollDirection", "fyne.ScrollDirection",
614+
"canvas.ImageFill", "canvas.ImageScale":
584615
f.SetInt(int64(reflect.ValueOf(v).Float()))
585616
case "fyne.TextStyle":
586617
f.Set(reflect.ValueOf(decodeTextStyle(reflect.ValueOf(v).Interface().(map[string]interface{}))))

0 commit comments

Comments
 (0)