Skip to content

Proposal: Use Vectors to Represent Position, Size, Etc.

Andy Williams edited this page Apr 2, 2024 · 6 revisions

Note

This document is archived as it refers to a design process.

Update 2020-12-31: this proposal has been implemented in #1661.


I'm new around here, and this was something that niggled me while I was writing my first application to get a feel for Fyne.

There are at least two different types: fyne.Position and fyne.Size that are different ways of thinking of a 2-tuple.

I propose that the geometry.go should be modified to be backed by a common vector/matrix type. This would make polymorphism between the Position and Size types easier. This also possibly gives an avenue by which to solve #835. The proposed approach here does break compatibility with the existing types though, to accommodate switching from a struct-based position and size type to an interface-based approach. However, refactoring code to instead use the vector type would be very straightforward.

Using this interface-based style also has the advantage of allowing polymorphism with other future types we may not be able to anticipate yet. It would also allow us to wrap third-party types, which may be convenient for us or for other developers using our library.

The following code is provided to illustrate this idea:

package main

import (
	"fmt"
	"math"
)

type Vec2 struct {
	X float64
	Y float64
}

func NewVec2(x, y float64) Vec2 {
	return Vec2{x, y}
}

func (v Vec2) Add(u Vec2) Vec2 {
	return NewVec2(u.X+v.X, u.Y+v.Y)
}

func (v Vec2) String() string {
	return fmt.Sprintf("[%f, %f]", v.X, v.Y)
}

type Position interface {
	GetX() float64
	GetY() float64
}

func (v Vec2) GetX() float64 {
	return v.X
}

func (v Vec2) GetY() float64 {
	return v.Y
}

type Size interface {
	GetWidth() float64
	GetHeight() float64
	Max(Size) Size
}

func (v Vec2) GetWidth() float64 {
	return v.X
}

func (v Vec2) GetHeight() float64 {
	return v.Y
}

func (s1 Vec2) Max(s2 Vec2) Vec2 {
	return NewVec2(math.Max(s1.GetWidth(), s2.GetWidth()), math.Max(s1.GetHeight(), s2.GetHeight()))
}

func main() {
	v1 := NewVec2(1, 2)
	v2 := NewVec2(2, 3.5)
	v3 := v1.Add(v2)

	fmt.Printf("v1=%s, v2=%s, v3=%s\n", v1, v2, v3)

	fmt.Printf("v1.Max(v2)=%s\n", v1.Max(v2))
}

It would be worthwhile to consider and discuss the merits of having GetX(), GetY() and GetWidth() GetHeight() methods, versus just updating the things that operate on Position and Size to use the X and Y fields of Vec2 directly, which might be simpler, though is potentially harder to read (I would argue that this is a common enough convention in UI toolkits that the additionally difficulty to read such could would be negligible).

As a point in favor of the interface-based approach, it would become very easy to build say a PhysicalScreenPosition interface that returned integers, abstracting over the typecast inside of the interface methods.

It may also be worth considering whether or not the same types are sensible for both drawing to the screen, and for developers to implement canvases with. There is some (valid) concern about resource utilization with floating point values. This makes sense for implementing arbitrary geometry, but maybe not when rendering buttons.


Edit 1: Following further discussion, here are some variations on this idea that might also be worth considering.

Directly declaring type Position Vec2 and so on:

package main

import (
	"fmt"
	"math"
)

type Vec2 struct {
	X float64
	Y float64
}

func NewVec2(x, y float64) Vec2 {
	return Vec2{x, y}
}

func (v Vec2) Add(u Vec2) Vec2 {
	return NewVec2(u.X+v.X, u.Y+v.Y)
}

func (v Vec2) String() string {
	return fmt.Sprintf("[%f, %f]", v.X, v.Y)
}

type Position Vec2

type Size Vec2

func (s1 Size) Max(s2 Size) Size {
	return Size(NewVec2(math.Max(s1.X, s2.X), math.Max(s1.Y, s2.Y)))
}

func main() {
	v1 := NewVec2(1, 2)
	v2 := NewVec2(2, 3.5)
	v3 := v1.Add(v2)

	fmt.Printf("v1=%s, v2=%s, v3=%s\n", v1, v2, v3)

	fmt.Printf("v1.Max(v2)=%s\n", Size(v1).Max(Size(v2)))
}

The same thing, using complex64:

package main

import (
	"fmt"
	"math"
)

type Vec2 complex64

func NewVec2(x, y float32) Vec2 {
	return Vec2(complex(x, y))
}

func (v Vec2) Add(u Vec2) Vec2 {
	return Vec2(complex64(v) + complex64(u))
}

func (v Vec2) X() float32 {
	return real(v)
}

func (v Vec2) Y() float32 {
	return imag(v)
}

func (v Vec2) String() string {
	return fmt.Sprintf("[%f, %f]", v.X(), v.Y())
}

type Position Vec2

type Size Vec2

func (s Size) Width() float32 {
	return Vec2(s).X()
}

func (s Size) Height() float32 {
	return Vec2(s).X()
}

func (s1 Size) Max(s2 Size) Size {
	// return Size(NewVec2(float32(math.Max(float64(s1.X()), float64*s2.X())), float32(math.Max(float64(s1.Y()), float64(s2.Y())))))
	width := float32(math.Max(float64(s1.Width()), float64(s2.Width())))
	height := float32(math.Max(float64(s1.Height()), float64(s2.Height())))
	return Size(NewVec2(width, height))
}

func (s Size) String() string {
	return fmt.Sprintf("%f x %f", s.Width(), s.Height())
}

func main() {
	v1 := NewVec2(1, 2)
	v2 := NewVec2(2, 3.5)
	v3 := v1.Add(v2)

	fmt.Printf("v1=%s, v2=%s, v3=%s\n", v1, v2, v3)

	fmt.Printf("v1.Max(v2)=%s\n", Size(v1).Max(Size(v2)))
}

(I think previous one was better, I don't think the complex64 thing is the way to go --Charles)