From 781f6a10ec10a85a8507806c66f5f8c48250c836 Mon Sep 17 00:00:00 2001 From: mstar Date: Mon, 3 Feb 2025 16:41:15 +0100 Subject: [PATCH] Works now, also reorganised code --- constants.go | 7 -- go.mod | 2 +- go.sum | 2 + main.go | 42 ++++---- shared/constants.go | 14 +++ shared/theming.go | 6 ++ shared/window.go | 46 +++++++++ windowState.go => shared/windowState.go | 7 +- timer/windowCountdown.go | 88 ++++++++++++++++ timer/windowEnterTime.go | 129 ++++++++++++++++++++++++ window.go | 62 ------------ windowEnterTime.go | 81 --------------- 12 files changed, 316 insertions(+), 170 deletions(-) delete mode 100644 constants.go create mode 100644 shared/constants.go create mode 100644 shared/theming.go create mode 100644 shared/window.go rename windowState.go => shared/windowState.go (74%) create mode 100644 timer/windowCountdown.go create mode 100644 timer/windowEnterTime.go delete mode 100644 window.go delete mode 100644 windowEnterTime.go diff --git a/constants.go b/constants.go deleted file mode 100644 index 9fd6db3..0000000 --- a/constants.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "image/color" - -var ( - ColorRed = color.NRGBA{R: 0xff, G: 0x0, B: 0x0, A: 255} -) diff --git a/go.mod b/go.mod index 2c288ff..7ce9d91 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.5 require ( gioui.org v0.8.0 // indirect gioui.org/shader v1.0.8 // indirect - git.mstar.dev/mstar/goutils v1.6.1 // indirect + git.mstar.dev/mstar/goutils v1.6.2 // indirect github.com/go-text/typesetting v0.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect diff --git a/go.sum b/go.sum index e8a7bac..98c8fd6 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= git.mstar.dev/mstar/goutils v1.6.1 h1:2yr9GYN8CJByZsJRu1pZ6WBp51Nn+3zJq49ky54xYDk= git.mstar.dev/mstar/goutils v1.6.1/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= +git.mstar.dev/mstar/goutils v1.6.2 h1:yqpEXQJWWiNZaJ2aenG5iCtjNP/gEnX9h5Ui4hrQ1tw= +git.mstar.dev/mstar/goutils v1.6.2/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= diff --git a/main.go b/main.go index 116d0ae..f34abf1 100644 --- a/main.go +++ b/main.go @@ -2,32 +2,40 @@ package main import ( "os" - "time" + "sync" "gioui.org/app" + "git.mstar.dev/mstar/goutils/other" + "git.mstar.dev/mstar/timer/shared" + "git.mstar.dev/mstar/timer/timer" "github.com/rs/zerolog/log" ) -var t time.Time - func main() { - window := new(app.Window) - var err error - t, err = time.Parse(time.RFC3339, "2025-01-31T22:50:00Z") - if err != nil { - log.Fatal().Err(err).Send() + other.SetupFlags() + other.ConfigureLoggingFromCliArgs() + // Create window struct outside. Doesn't cause any renders yet + windows := []*app.Window{ + new(app.Window), + } + var wg sync.WaitGroup + for _, window := range windows { + wg.Add(1) + go windowLauncher(window, &wg) } go func() { - // window.Option(func(m unit.Metric, c *app.Config) { - // c.Size = image.Pt(500, 150) - // c.MaxSize = image.Pt(500, 150) - // c.MinSize = image.Pt(500, 150) - // }) - err := run(window, &StateEnterTime{}) - if err != nil { - log.Fatal().Err(err).Msg("Failed to run main window") - } + wg.Wait() + log.Info().Msg("All windows closed, exiting") os.Exit(0) }() app.Main() } + +func windowLauncher(window *app.Window, wg *sync.WaitGroup) { + defer wg.Done() + log.Info().Msg("Starting window") + err := shared.Run(window, &timer.StateEnterTime{}) + if err != nil { + log.Fatal().Err(err).Msg("Failed to run main window") + } +} diff --git a/shared/constants.go b/shared/constants.go new file mode 100644 index 0000000..acbd039 --- /dev/null +++ b/shared/constants.go @@ -0,0 +1,14 @@ +package shared + +import ( + "errors" + "image/color" +) + +var ( + ColorRed = color.NRGBA{R: 0xff, G: 0x0, B: 0x0, A: 255} +) + +var ( + ErrExitOk = errors.New("exit ok") +) diff --git a/shared/theming.go b/shared/theming.go new file mode 100644 index 0000000..3c33aa0 --- /dev/null +++ b/shared/theming.go @@ -0,0 +1,6 @@ +package shared + +import "image/color" + +var BackgroundColor = color.NRGBA{R: 0x0, G: 0x0, B: 0x0, A: 0xff} +var TextColor = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} diff --git a/shared/window.go b/shared/window.go new file mode 100644 index 0000000..bd6013c --- /dev/null +++ b/shared/window.go @@ -0,0 +1,46 @@ +package shared + +import ( + "sync" + + "gioui.org/app" + "gioui.org/widget/material" + "github.com/rs/zerolog/log" +) + +type GlobalState struct { + Window *app.Window + Theme *material.Theme + state WindowState +} + +func Run(window *app.Window, initialState WindowState) error { + // Set up global data first + state := GlobalState{} + theme := material.NewTheme() + theme.Bg = BackgroundColor + theme.Fg = TextColor + state.Theme = theme + state.Window = window + state.state = initialState + // Then start showing the window and polling for events + // Though technically, I don't think window.Event will cause the window to display yet + // since what should it display? It doesn't have anything to display. So first display happens at event.Frame + for { + e := window.Event() + // Let the current view handle the event, then update accordingly + newState := state.state.Run(&state, e) + if newState.NextState != nil { + state.state = newState.NextState + } + if newState.ExitCode != nil { + log.Info().Msg("Received shutdown request") + wg := sync.WaitGroup{} + wg.Add(1) + go state.state.Exit(&state, &wg) + wg.Wait() + log.Info().Msg("Shutdown of window complete") + return nil + } + } +} diff --git a/windowState.go b/shared/windowState.go similarity index 74% rename from windowState.go rename to shared/windowState.go index 438b969..8e01569 100644 --- a/windowState.go +++ b/shared/windowState.go @@ -1,18 +1,21 @@ -package main +package shared import ( + "sync" + "gioui.org/io/event" ) type WindowState interface { Run(globalState *GlobalState, event event.Event) NewState + Exit(globalState *GlobalState, wg *sync.WaitGroup) } type NewState struct { // If null, keep current state NextState WindowState Error error - ExitCode *int + ExitCode error } func EmptyEvent() NewState { diff --git a/timer/windowCountdown.go b/timer/windowCountdown.go new file mode 100644 index 0000000..d6d9d0a --- /dev/null +++ b/timer/windowCountdown.go @@ -0,0 +1,88 @@ +package timer + +import ( + "image" + "sync" + "time" + + "gioui.org/app" + "gioui.org/io/event" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/widget/material" + "git.mstar.dev/mstar/timer/shared" + "github.com/rs/zerolog/log" +) + +type StateCountdown struct { + initialSetupDone bool + TargetTime time.Time + ops op.Ops +} + +func (s *StateCountdown) Run(globalState *shared.GlobalState, event event.Event) shared.NewState { + if !s.initialSetupDone { + s.initialSetup(globalState) + } + switch event := event.(type) { + case app.FrameEvent: + return s.handleFrameEvent(globalState, &event) + case app.DestroyEvent: + return s.handleQuitEvent() + case app.WaylandViewEvent, app.X11ViewEvent: + return shared.EmptyEvent() + default: + log.Debug().Any("event", event).Type("type", event).Msg("Unknown event") + return shared.NewState{} + } +} + +func (s *StateCountdown) Exit(globalState *shared.GlobalState, wg *sync.WaitGroup) { + wg.Done() +} + +func (s *StateCountdown) initialSetup(globalState *shared.GlobalState) { + s.initialSetupDone = true + go func(window *app.Window) { + t := time.NewTicker(time.Second) + for range t.C { + window.Invalidate() + } + }(globalState.Window) +} + +func (s *StateCountdown) handleQuitEvent() shared.NewState { + return shared.NewState{ExitCode: shared.ErrExitOk} +} + +func (s *StateCountdown) handleFrameEvent( + globalState *shared.GlobalState, + frameEvent *app.FrameEvent, +) shared.NewState { + gtx := app.NewContext(&s.ops, *frameEvent) + paint.FillShape( + gtx.Ops, + globalState.Theme.Bg, + clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op(), + ) + diff := frameEvent.Now.Sub(s.TargetTime) + diff *= -1 + title := material.H1( + globalState.Theme, + diff.Round(time.Second).String(), + ) + // // + // // // Change the color of the label. + title.Color = shared.TextColor + // // + // // // Change the position of the label. + title.Alignment = text.Middle + // + // // Draw the label to the graphics context. + title.Layout(gtx) + // // Pass the drawing operations to the GPU. + frameEvent.Frame(&s.ops) + return shared.NewState{} +} diff --git a/timer/windowEnterTime.go b/timer/windowEnterTime.go new file mode 100644 index 0000000..af2307b --- /dev/null +++ b/timer/windowEnterTime.go @@ -0,0 +1,129 @@ +package timer + +import ( + "image" + "sync" + "time" + + "gioui.org/app" + "gioui.org/io/event" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + "git.mstar.dev/mstar/goutils/other" + "git.mstar.dev/mstar/timer/shared" + "github.com/rs/zerolog/log" +) + +type StateEnterTime struct { + initialSetupDone bool + editor *widget.Editor + inputField *material.EditorStyle + inputErrorMessage *string + ops op.Ops +} + +func (s *StateEnterTime) initialSetup(globalState *shared.GlobalState) { + s.editor = &widget.Editor{ + SingleLine: true, + Submit: true, + Filter: "01234567890TZ+-:", + } + field := material.Editor(globalState.Theme, s.editor, "timestamp") + s.inputField = &field + s.initialSetupDone = true + globalState.Window.Option(func(m unit.Metric, c *app.Config) { + c.MaxSize.X = 520 + c.MaxSize.Y = 150 + c.MinSize = c.MaxSize + c.Size = c.MaxSize + }) +} + +func (s *StateEnterTime) Run(globalState *shared.GlobalState, event event.Event) shared.NewState { + if !s.initialSetupDone { + s.initialSetup(globalState) + } + switch event := event.(type) { + case app.FrameEvent: + return s.handleFrameEvent(globalState, &event) + case app.DestroyEvent: + return s.handleQuitEvent() + case app.WaylandViewEvent, app.X11ViewEvent: + return shared.EmptyEvent() + default: + log.Debug().Any("event", event).Type("type", event).Msg("Unknown event") + return shared.NewState{} + } +} + +func (s *StateEnterTime) Exit(globalState *shared.GlobalState, wg *sync.WaitGroup) { + wg.Done() +} + +func (s *StateEnterTime) handleFrameEvent( + globalState *shared.GlobalState, + frameEvent *app.FrameEvent, +) shared.NewState { + gtx := app.NewContext(&s.ops, *frameEvent) + for ev, ok := s.editor.Update(gtx); ok; ev, ok = s.editor.Update(gtx) { + switch ev := ev.(type) { + case widget.SubmitEvent: + t, err := time.Parse(time.RFC3339, ev.Text) + if err == nil { + return s.handleTimeEntered(t) + } + s.inputErrorMessage = other.IntoPointer(err.Error()) + log.Debug().Err(err).Msg("Invalid timestamp") + } + log.Debug().Any("Editor event", ev).Type("type", ev).Send() + } + paint.FillShape( + gtx.Ops, + globalState.Theme.Bg, + clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op(), + ) + label := material.H3(globalState.Theme, "Enter timestamp") + hint := material.Subtitle1( + globalState.Theme, + "Format: --
T::(Z|+)", + ) + var errorMessage material.LabelStyle + nrOfElements := 3 + if s.inputErrorMessage != nil { + nrOfElements++ + errorMessage = material.Subtitle2(globalState.Theme, *s.inputErrorMessage) + } + list := layout.List{Axis: layout.Vertical} + list.Layout(gtx, nrOfElements, func(gtx layout.Context, index int) layout.Dimensions { + switch index { + case 0: + return label.Layout(gtx) + case 1: + return hint.Layout(gtx) + case 2: + return s.inputField.Layout(gtx) + case 3: + return errorMessage.Layout(gtx) + default: + return hint.Layout(gtx) + } + }) + frameEvent.Frame(&s.ops) + return shared.NewState{} +} + +func (s *StateEnterTime) handleQuitEvent() shared.NewState { + return shared.NewState{ExitCode: shared.ErrExitOk} +} + +func (s *StateEnterTime) handleTimeEntered(stamp time.Time) shared.NewState { + // TODO: Switch to countdown state once that is implemented + return shared.NewState{ + NextState: &StateCountdown{TargetTime: stamp}, + } +} diff --git a/window.go b/window.go deleted file mode 100644 index 3ba33b7..0000000 --- a/window.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "image/color" - "os" - "time" - - "gioui.org/app" - "gioui.org/widget/material" -) - -type GlobalState struct { - Window *app.Window - Theme *material.Theme - state WindowState -} - -func ticker(window *app.Window) { - t := time.NewTicker(time.Second) - for range t.C { - window.Invalidate() - } -} - -func run(window *app.Window, initialState WindowState) error { - state := GlobalState{} - theme := material.NewTheme() - theme.Bg = color.NRGBA{A: 0xff} - state.Theme = theme - state.Window = window - state.state = initialState - for { - e := window.Event() - newState := state.state.Run(&state, e) - if newState.NextState != nil { - state.state = newState.NextState - } - if newState.ExitCode != nil { - os.Exit(*newState.ExitCode) - } - // log.Debug().Any("size", e.Size).Msg("Redrawing") - // // This graphics context is used for managing the rendering state. - // gtx := app.NewContext(&ops, e) - // paint.FillShape(gtx.Ops, theme.Bg, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op()) - // // - // // // Define an large label with an appropriate text: - // log.Debug().Dur("d", t.Sub(e.Now)).Time("t", t).Send() - // title := material.H1(theme, e.Now.Sub(t).Round(time.Second).String()) - // // - // // // Change the color of the label. - // maroon := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 255} - // title.Color = maroon - // // - // // // Change the position of the label. - // title.Alignment = text.Middle - // - // // Draw the label to the graphics context. - // title.Layout(gtx) - // // Pass the drawing operations to the GPU. - // e.Frame(&ops) - } -} diff --git a/windowEnterTime.go b/windowEnterTime.go deleted file mode 100644 index 7cfb57a..0000000 --- a/windowEnterTime.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "gioui.org/app" - "gioui.org/io/event" - "gioui.org/layout" - "gioui.org/op" - "gioui.org/widget" - "gioui.org/widget/material" - "git.mstar.dev/mstar/goutils/other" - "github.com/rs/zerolog/log" -) - -type StateEnterTime struct { - initialSetupDone bool - editor *widget.Editor - inputField *material.EditorStyle - ops op.Ops -} - -func (s *StateEnterTime) initialSetup(globalState *GlobalState) { - s.editor = &widget.Editor{ - SingleLine: true, - Submit: true, - Filter: "01234567890TZ+-:", - } - field := material.Editor(globalState.Theme, s.editor, "timestamp") - s.inputField = &field - s.initialSetupDone = true -} - -func (s *StateEnterTime) Run(globalState *GlobalState, event event.Event) NewState { - if !s.initialSetupDone { - s.initialSetup(globalState) - } - switch event := event.(type) { - case app.FrameEvent: - return s.handleFrameEvent(globalState, &event) - case app.DestroyEvent: - return s.handleQuitEvent() - case app.WaylandViewEvent, app.X11ViewEvent: - return EmptyEvent() - default: - log.Debug().Any("event", event).Type("type", event).Msg("Unknown event") - return NewState{} - } -} - -func (s *StateEnterTime) handleFrameEvent( - globalState *GlobalState, - frameEvent *app.FrameEvent, -) NewState { - gtx := app.NewContext(&s.ops, *frameEvent) - for ev, ok := s.editor.Update(gtx); ok; ev, ok = s.editor.Update(gtx) { - log.Debug().Any("Editor event", ev).Send() - } - label := material.H3(globalState.Theme, "Enter timestamp") - hint := material.Subtitle1( - globalState.Theme, - "Format: --
T::(Z|+)", - ) - list := layout.List{Axis: layout.Vertical} - list.Layout(gtx, 3, func(gtx layout.Context, index int) layout.Dimensions { - switch index { - case 0: - return label.Layout(gtx) - case 1: - return hint.Layout(gtx) - case 2: - return s.inputField.Layout(gtx) - default: - return hint.Layout(gtx) - } - }) - frameEvent.Frame(&s.ops) - return NewState{} -} - -func (s *StateEnterTime) handleQuitEvent() NewState { - return NewState{ExitCode: other.IntoPointer(0)} -}