Move logic into separate internal/editor package

- Create internal/editor/state.go with State struct and EditorLayout
- Create internal/editor/logic.go with Logic struct and Run method
- Logic owns all state, provides channels for config, frames, input, results
- Main goroutine uses editor.NewLogic() and logic.Run()
- Proper separation for testing later

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Greg Pomerantz 2026-05-08 17:06:00 -04:00
parent 556f72643f
commit 013416ee08
4 changed files with 170 additions and 36 deletions

View File

@ -10,6 +10,7 @@ import (
"gioui.org/text"
"gioui.org/unit"
"pad/internal/editor"
"pad/internal/ui"
)
@ -32,27 +33,22 @@ func run(w *app.Window) error {
shaper := text.NewShaper()
renderer := ui.New(ui.Theme{FontSize: 14}, shaper)
// Channels for communication between goroutines
configChan := make(chan ui.ConfigEvent)
frameChan := make(chan []ui.Element)
inputChan := make(chan []ui.InputEvent)
// Initial screen size
screenWidth := unit.Dp(390)
screenHeight := unit.Dp(844)
// Create logic instance
logic := editor.NewLogic(screenWidth, screenHeight)
// Shared state protected by mutex
var mu sync.Mutex
var elems []ui.Element
// Initial screen size
screenWidth := unit.Dp(390)
screenHeight := unit.Dp(844)
// Compute initial elements
elems = ui.EditorLayout(screenWidth, screenHeight)
// Start logic goroutine
go logic(w, shaper, screenWidth, screenHeight, configChan, frameChan, inputChan)
go logic.Run()
// Start frame receiver goroutine
go frameReceiver(w, &mu, &elems, frameChan)
go frameReceiver(w, &mu, &elems, logic.FrameChan())
// Main event loop
for {
@ -61,7 +57,7 @@ func run(w *app.Window) error {
return e.Err
case app.ConfigEvent:
// Send config event to logic goroutine
configChan <- ui.ConfigEvent{
logic.ConfigChan() <- editor.ConfigEvent{
Width: unit.Dp(e.Config.Size.X),
Height: unit.Dp(e.Config.Size.Y),
}
@ -78,28 +74,6 @@ func run(w *app.Window) error {
}
}
// logic runs in a separate goroutine and owns all application state.
func logic(w *app.Window, shaper *text.Shaper, screenWidth, screenHeight unit.Dp,
configChan <-chan ui.ConfigEvent, frameChan chan<- []ui.Element, inputChan <-chan []ui.InputEvent) {
// Initial elements
elems := ui.EditorLayout(screenWidth, screenHeight)
for {
select {
case cfg := <-configChan:
// Update screen size and recompute layout
screenWidth = cfg.Width
screenHeight = cfg.Height
elems = ui.EditorLayout(screenWidth, screenHeight)
frameChan <- elems
case <-inputChan:
// Process input (not implemented in mockup)
frameChan <- elems
}
}
}
// frameReceiver runs in a separate goroutine and bridges frameChan to the main loop.
func frameReceiver(w *app.Window, mu *sync.Mutex, elems *[]ui.Element, frameChan <-chan []ui.Element) {
for {

90
internal/editor/logic.go Normal file
View File

@ -0,0 +1,90 @@
package editor
import (
"gioui.org/unit"
"pad/internal/ui"
)
// ConfigEvent represents a window configuration change (resize, orientation).
type ConfigEvent struct {
Width unit.Dp
Height unit.Dp
}
// InputEvent represents a user input event routed to an element.
type InputEvent struct {
ElementID string
Type ui.InputType
Data any
}
// ResultEvent represents a completed async task result.
type ResultEvent struct {
// Future: add result fields here
}
// Logic runs the logic goroutine and provides channels for communication.
type Logic struct {
state *State
configChan chan ConfigEvent
frameChan chan []ui.Element
inputChan chan []InputEvent
resultChan chan ResultEvent
}
// NewLogic creates a new Logic instance.
func NewLogic(screenWidth, screenHeight unit.Dp) *Logic {
return &Logic{
state: NewState(screenWidth, screenHeight),
configChan: make(chan ConfigEvent),
frameChan: make(chan []ui.Element),
inputChan: make(chan []InputEvent),
resultChan: make(chan ResultEvent),
}
}
// ConfigChan returns the config channel for the logic goroutine.
func (l *Logic) ConfigChan() chan<- ConfigEvent {
return l.configChan
}
// FrameChan returns the frame channel for the logic goroutine.
func (l *Logic) FrameChan() <-chan []ui.Element {
return l.frameChan
}
// InputChan returns the input channel for the logic goroutine.
func (l *Logic) InputChan() chan<- []InputEvent {
return l.inputChan
}
// ResultChan returns the result channel for the logic goroutine.
func (l *Logic) ResultChan() <-chan ResultEvent {
return l.resultChan
}
// Run runs the logic goroutine loop.
func (l *Logic) Run() {
for {
select {
case cfg := <-l.configChan:
// Update screen size and recompute layout
l.state.ScreenWidth = cfg.Width
l.state.ScreenHeight = cfg.Height
l.state.Elems = EditorLayout(cfg.Width, cfg.Height)
l.frameChan <- l.state.Elems
case <-l.inputChan:
// Process input (not implemented in mockup)
l.frameChan <- l.state.Elems
case <-l.resultChan:
// Handle result (not implemented in mockup)
l.frameChan <- l.state.Elems
}
}
}
// State returns the current state.
func (l *Logic) State() *State {
return l.state
}

70
internal/editor/state.go Normal file
View File

@ -0,0 +1,70 @@
package editor
import (
"gioui.org/unit"
"pad/internal/ui"
)
// State holds all application state owned by the logic goroutine.
type State struct {
ScreenWidth unit.Dp
ScreenHeight unit.Dp
Elems []ui.Element
}
// NewState creates a new State with initial editor layout.
func NewState(screenWidth, screenHeight unit.Dp) *State {
return &State{
ScreenWidth: screenWidth,
ScreenHeight: screenHeight,
Elems: EditorLayout(screenWidth, screenHeight),
}
}
// EditorLayout computes regions for the editor page.
// It takes screen dimensions and returns []Element.
func EditorLayout(screenWidth, screenHeight unit.Dp) []ui.Element {
// Compute StatusBar region
// Line 1: filename (24 DP)
// Line 2: icons (24 DP)
statusBarHeight := unit.Dp(48)
statusBarRegion := ui.Region{
X: 0,
Y: 0,
W: screenWidth,
H: statusBarHeight,
}
// Compute BottomBar region (fixed height, at bottom of screen)
bottomBarHeight := unit.Dp(24)
bottomBarY := screenHeight - bottomBarHeight
bottomBarRegion := ui.Region{
X: 0,
Y: bottomBarY,
W: screenWidth,
H: bottomBarHeight,
}
// Create StatusBar with mocked filename
statusBar := ui.NewStatusBar(
statusBarRegion,
"very_long_filename_that_does_not_fit_on_a_single_line.txt",
false, // FilenameExp
true, // CutCopy
false, // Copy
true, // Paste
false, // ConflictIcon
true, // Search
)
// Create BottomBar with mocked data
bottomBar := ui.NewBottomBar(
bottomBarRegion,
"Ln 47, Col 12",
"1024 / 50000",
true, // WordWrap
)
return []ui.Element{statusBar, bottomBar}
}

BIN
pad

Binary file not shown.