add(project): initial commit

This commit is contained in:
maotovisk 2025-07-28 08:43:15 -03:00
parent f34565bf49
commit cf17a4ec59
11 changed files with 628 additions and 0 deletions

7
cmd/simple_ui/main.go Normal file
View file

@ -0,0 +1,7 @@
package main
import "simple_ui/internal/app"
func main() {
app.Run()
}

15
go.mod Normal file
View 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
View 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
View 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
)

View 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{}, ""
}

View 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)
})
})
}

View 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
View 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
View 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
View file

@ -0,0 +1,9 @@
package ui
import (
"gioui.org/layout"
)
type Page interface {
Layout(gtx layout.Context) layout.Dimensions
}

View 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
}