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