-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Proposal: Use Vectors to Represent Position, Size, Etc.
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)