add(project): initial commit
This commit is contained in:
parent
f34565bf49
commit
cf17a4ec59
11 changed files with 628 additions and 0 deletions
7
cmd/simple_ui/main.go
Normal file
7
cmd/simple_ui/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "simple_ui/internal/app"
|
||||
|
||||
func main() {
|
||||
app.Run()
|
||||
}
|
||||
15
go.mod
Normal file
15
go.mod
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
module simple_ui
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require gioui.org v0.8.0
|
||||
|
||||
require (
|
||||
gioui.org/shader v1.0.8 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
|
||||
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
)
|
||||
17
go.sum
Normal file
17
go.sum
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
|
||||
gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
|
||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
|
||||
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
|
||||
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
101
internal/app/app.go
Normal file
101
internal/app/app.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"simple_ui/internal/components"
|
||||
"simple_ui/internal/ui"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
func Run() {
|
||||
go runApp()
|
||||
app.Main()
|
||||
}
|
||||
|
||||
func runApp() {
|
||||
w := &app.Window{}
|
||||
|
||||
w.Option(
|
||||
app.Title("Simple UI"),
|
||||
app.Size(800, 600),
|
||||
app.MinSize(400, 300),
|
||||
)
|
||||
|
||||
err := runWindowEvents(w)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func runWindowEvents(window *app.Window) error {
|
||||
theme := material.NewTheme()
|
||||
var ops op.Ops
|
||||
currentPage := HomePage
|
||||
|
||||
navMenu := components.NewNavigationMenu(theme)
|
||||
|
||||
homeIcon, err := widget.NewIcon(icons.ActionHome)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create home icon: %v", err)
|
||||
}
|
||||
aboutIcon, err := widget.NewIcon(icons.ActionInfo)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create about icon: %v", err)
|
||||
}
|
||||
|
||||
navMenu.AddItem("Home", homeIcon, func() { currentPage = HomePage })
|
||||
navMenu.AddItem("About", aboutIcon, func() { currentPage = AboutPage })
|
||||
|
||||
navMenu.SetSelected(int(currentPage))
|
||||
|
||||
pages := []ui.Page{
|
||||
HomePage: ui.NewHomePage(theme),
|
||||
AboutPage: ui.NewAboutPage(theme),
|
||||
}
|
||||
|
||||
for {
|
||||
switch e := window.Event().(type) {
|
||||
case app.DestroyEvent:
|
||||
return e.Err
|
||||
case app.FrameEvent:
|
||||
gtx := app.NewContext(&ops, e)
|
||||
|
||||
navMenu.HandleClicks(gtx)
|
||||
navMenu.SetSelected(int(currentPage))
|
||||
|
||||
layout.Flex{
|
||||
Axis: layout.Horizontal,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return navMenu.Layout(gtx)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: unit.Dp(16),
|
||||
Bottom: unit.Dp(16),
|
||||
Left: unit.Dp(16),
|
||||
Right: unit.Dp(16),
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return pages[currentPage].Layout(gtx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
e.Frame(gtx.Ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
HomePage = iota
|
||||
AboutPage
|
||||
)
|
||||
25
internal/components/dropdown.go
Normal file
25
internal/components/dropdown.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
type Dropdown struct {
|
||||
Editor widget.Editor
|
||||
Clicks []*widget.Clickable
|
||||
Options []string
|
||||
Filtered []string
|
||||
Show bool
|
||||
}
|
||||
|
||||
func NewDropdown(options []string) *Dropdown {
|
||||
return &Dropdown{
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dropdown) Layout(gtx layout.Context, th *material.Theme, hint string) (layout.Dimensions, string) {
|
||||
return layout.Dimensions{}, ""
|
||||
}
|
||||
89
internal/components/icon_button.go
Normal file
89
internal/components/icon_button.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
type ButtonState int
|
||||
|
||||
const (
|
||||
NormalButton ButtonState = iota
|
||||
SelectedButton
|
||||
UnselectedButton
|
||||
)
|
||||
|
||||
type iconAndTextButton struct {
|
||||
theme *material.Theme
|
||||
button *widget.Clickable
|
||||
icon *widget.Icon
|
||||
word string
|
||||
axis layout.Axis
|
||||
state ButtonState
|
||||
}
|
||||
|
||||
func (b iconAndTextButton) Layout(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.ButtonLayout(b.theme, b.button)
|
||||
switch b.state {
|
||||
case NormalButton:
|
||||
btn.Background = b.theme.Palette.Bg
|
||||
case SelectedButton:
|
||||
btn.Background = b.theme.Palette.ContrastBg
|
||||
case UnselectedButton:
|
||||
btn.Background = b.theme.Palette.Bg
|
||||
}
|
||||
|
||||
return btn.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
iconAndLabel := layout.Flex{Axis: b.axis, Alignment: layout.Middle}
|
||||
var bottomSpacer, textIconSpacer unit.Dp
|
||||
|
||||
if b.axis == layout.Horizontal {
|
||||
bottomSpacer = unit.Dp(0)
|
||||
textIconSpacer = unit.Dp(2)
|
||||
} else {
|
||||
bottomSpacer = unit.Dp(2)
|
||||
textIconSpacer = unit.Dp(0)
|
||||
}
|
||||
|
||||
layIcon := layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Bottom: bottomSpacer, Right: textIconSpacer}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
var d layout.Dimensions
|
||||
if b.icon != nil {
|
||||
size := gtx.Dp(unit.Dp(24))
|
||||
|
||||
if b.axis == layout.Horizontal {
|
||||
size = gtx.Dp(unit.Dp(56)) - 2*gtx.Dp(unit.Dp(16))
|
||||
}
|
||||
|
||||
gtx.Constraints = layout.Exact(image.Pt(size, size))
|
||||
if b.state == SelectedButton {
|
||||
d = b.icon.Layout(gtx, b.theme.Palette.ContrastFg)
|
||||
} else {
|
||||
d = b.icon.Layout(gtx, b.theme.Palette.Fg)
|
||||
}
|
||||
}
|
||||
return d
|
||||
})
|
||||
})
|
||||
|
||||
layLabel := layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{Top: bottomSpacer, Right: textIconSpacer}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
l := material.Body1(b.theme, b.word)
|
||||
if b.state == SelectedButton {
|
||||
l.Color = b.theme.Palette.ContrastFg
|
||||
} else {
|
||||
l.Color = b.theme.Palette.Fg
|
||||
}
|
||||
return l.Layout(gtx)
|
||||
})
|
||||
})
|
||||
|
||||
return iconAndLabel.Layout(gtx, layIcon, layLabel)
|
||||
})
|
||||
})
|
||||
}
|
||||
194
internal/components/navigation_menu.go
Normal file
194
internal/components/navigation_menu.go
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"simple_ui/internal/utils"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type NavigationMenu struct {
|
||||
theme *material.Theme
|
||||
contractButton *widget.Clickable
|
||||
items []MenuItem
|
||||
contracted bool
|
||||
selected int
|
||||
anim utils.Animation
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Button *widget.Clickable
|
||||
Icon *widget.Icon
|
||||
OnClick func()
|
||||
}
|
||||
|
||||
func NewNavigationMenu(theme *material.Theme) *NavigationMenu {
|
||||
nm := &NavigationMenu{
|
||||
theme: theme,
|
||||
contractButton: &widget.Clickable{},
|
||||
items: make([]MenuItem, 0),
|
||||
}
|
||||
nm.anim.Start(0, 0, 0, utils.EasingInOutSine)
|
||||
return nm
|
||||
}
|
||||
|
||||
func (nm *NavigationMenu) ToggleContracted() {
|
||||
nm.contracted = !nm.contracted
|
||||
var end float32
|
||||
if nm.contracted {
|
||||
end = 1
|
||||
} else {
|
||||
end = 0
|
||||
}
|
||||
nm.anim.Start(nm.anim.Current(), end, 250*time.Millisecond, utils.EasingInOutSine)
|
||||
fmt.Printf("Navigation menu toggled: %v\n", nm.contracted)
|
||||
}
|
||||
|
||||
func (nm *NavigationMenu) IsContracted() bool {
|
||||
return nm.contracted
|
||||
}
|
||||
|
||||
func (nm *NavigationMenu) AddItem(label string, icon *widget.Icon, onClick func()) *widget.Clickable {
|
||||
btn := &widget.Clickable{}
|
||||
nm.items = append(nm.items, MenuItem{
|
||||
Label: label,
|
||||
Button: btn,
|
||||
Icon: icon,
|
||||
OnClick: onClick,
|
||||
})
|
||||
return btn
|
||||
}
|
||||
|
||||
func (nm *NavigationMenu) SetSelected(index int) {
|
||||
if index >= 0 && index < len(nm.items) {
|
||||
nm.selected = index
|
||||
}
|
||||
}
|
||||
|
||||
func (nm *NavigationMenu) HandleClicks(gtx layout.Context) {
|
||||
for i, item := range nm.items {
|
||||
if item.Button.Clicked(gtx) {
|
||||
nm.selected = i
|
||||
if item.OnClick != nil {
|
||||
item.OnClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (nm *NavigationMenu) Layout(gtx layout.Context) layout.Dimensions {
|
||||
// Animate contraction/expansion
|
||||
value := nm.anim.Update(gtx)
|
||||
contractedWidth := float32(gtx.Dp(unit.Dp(100)))
|
||||
expandedWidth := float32(gtx.Dp(unit.Dp(240)))
|
||||
width := int(expandedWidth + (contractedWidth-expandedWidth)*value)
|
||||
gtx.Constraints.Min.X = width
|
||||
gtx.Constraints.Max.X = width
|
||||
|
||||
// Add background color
|
||||
backgroundColor := color.NRGBA{R: 205, G: 205, B: 205, A: 255} // Blue
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect{Max: gtx.Constraints.Min}.Op())
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Horizontal,
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if nm.contracted {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return material.H6(nm.theme, "Simple Weather").Layout(gtx)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Width: unit.Dp(16)}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
contractedIcon, err := widget.NewIcon(icons.NavigationChevronLeft)
|
||||
if err != nil {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
if nm.IsContracted() {
|
||||
contractedIcon, err = widget.NewIcon(icons.NavigationChevronRight)
|
||||
if err != nil {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
}
|
||||
|
||||
if nm.contractButton.Clicked(gtx) {
|
||||
nm.ToggleContracted()
|
||||
}
|
||||
contractButton := material.IconButton(nm.theme, nm.contractButton, contractedIcon, "Toggle menu")
|
||||
contractButton.Background = nm.theme.Palette.Bg
|
||||
contractButton.Color = nm.theme.Palette.Fg
|
||||
|
||||
return contractButton.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx, nm.menuItems()...)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (nm *NavigationMenu) menuItems() []layout.FlexChild {
|
||||
children := make([]layout.FlexChild, len(nm.items))
|
||||
|
||||
for i, item := range nm.items {
|
||||
i, item := i, item
|
||||
children[i] = layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
var state ButtonState
|
||||
|
||||
if i == nm.selected {
|
||||
state = SelectedButton
|
||||
} else {
|
||||
state = UnselectedButton
|
||||
}
|
||||
axis := layout.Horizontal
|
||||
|
||||
if nm.IsContracted() {
|
||||
axis = layout.Vertical
|
||||
}
|
||||
|
||||
btn := &iconAndTextButton{
|
||||
theme: nm.theme,
|
||||
button: item.Button,
|
||||
icon: item.Icon,
|
||||
word: item.Label,
|
||||
state: state,
|
||||
axis: axis,
|
||||
}
|
||||
|
||||
return layout.Inset{Top: unit.Dp(2), Bottom: unit.Dp(2), Left: unit.Dp(4), Right: unit.Dp(4)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return btn.Layout(gtx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
29
internal/ui/about_page.go
Normal file
29
internal/ui/about_page.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
type AboutPage struct {
|
||||
theme *material.Theme
|
||||
}
|
||||
|
||||
func NewAboutPage(theme *material.Theme) *AboutPage {
|
||||
return &AboutPage{
|
||||
theme: theme,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AboutPage) Layout(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(16).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
body := material.Body1(p.theme, "This is a simple UI application built with Gio.")
|
||||
return body.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
63
internal/ui/home.go
Normal file
63
internal/ui/home.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
type HomePage struct {
|
||||
searchButton *widget.Clickable
|
||||
searchField *widget.Editor
|
||||
theme *material.Theme
|
||||
}
|
||||
|
||||
func NewHomePage(theme *material.Theme) *HomePage {
|
||||
return &HomePage{
|
||||
searchButton: &widget.Clickable{},
|
||||
theme: theme,
|
||||
searchField: &widget.Editor{SingleLine: true},
|
||||
}
|
||||
}
|
||||
|
||||
func searchClicked(searchField *widget.Editor) {
|
||||
query := searchField.Text()
|
||||
searchField.SetText("")
|
||||
println("Search query:", query)
|
||||
}
|
||||
|
||||
func (h *HomePage) Layout(gtx layout.Context) layout.Dimensions {
|
||||
return layout.UniformInset(16).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Horizontal,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions {
|
||||
return material.Editor(h.theme, h.searchField, "Search...").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if h.searchButton.Clicked(gtx) {
|
||||
searchClicked(h.searchField)
|
||||
}
|
||||
return material.Button(h.theme, h.searchButton, "Search").Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
return layout.Dimensions{}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
9
internal/ui/page.go
Normal file
9
internal/ui/page.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
)
|
||||
|
||||
type Page interface {
|
||||
Layout(gtx layout.Context) layout.Dimensions
|
||||
}
|
||||
79
internal/utils/animation.go
Normal file
79
internal/utils/animation.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
)
|
||||
|
||||
type Animation struct {
|
||||
start float32
|
||||
end float32
|
||||
current float32
|
||||
startTime time.Time
|
||||
duration time.Duration
|
||||
active bool
|
||||
Easing Easing
|
||||
}
|
||||
|
||||
type Easing int
|
||||
|
||||
const (
|
||||
None Easing = iota
|
||||
EasingIn
|
||||
EasingOut
|
||||
EasingOutSine
|
||||
EasingInOutSine
|
||||
)
|
||||
|
||||
func (a *Animation) Current() float32 {
|
||||
return a.current
|
||||
}
|
||||
|
||||
func applyEasing(easing Easing, t float32) float32 {
|
||||
switch easing {
|
||||
case EasingIn:
|
||||
return t * t
|
||||
case EasingOut:
|
||||
return t * (2 - t)
|
||||
case EasingOutSine:
|
||||
return float32(math.Sin(float64(t) * math.Pi / 2))
|
||||
case EasingInOutSine:
|
||||
return float32(-0.5 * (math.Cos(math.Pi*float64(t)) - 1))
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Animation) Start(start, end float32, duration time.Duration, easing Easing) {
|
||||
a.start = start
|
||||
a.end = end
|
||||
a.current = start
|
||||
a.startTime = time.Now()
|
||||
a.duration = duration
|
||||
a.Easing = easing
|
||||
a.active = true
|
||||
}
|
||||
|
||||
func (a *Animation) Update(gtx layout.Context) float32 {
|
||||
if !a.active {
|
||||
return a.current
|
||||
}
|
||||
|
||||
elapsed := time.Since(a.startTime)
|
||||
|
||||
if elapsed >= a.duration {
|
||||
a.current = a.end
|
||||
a.active = false
|
||||
} else {
|
||||
progress := float32(elapsed) / float32(a.duration)
|
||||
eased := applyEasing(a.Easing, progress)
|
||||
a.current = a.start + (a.end-a.start)*eased
|
||||
}
|
||||
|
||||
gtx.Execute(&op.InvalidateCmd{})
|
||||
|
||||
return a.current
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue