- Replace gtx.Event(nil) with key.Filter{Focus: focusedID} and
key.FocusFilter{Target: focusedID} so Gio correctly routes key
and edit events to the focused editor text field.
- Convert EditorState.CursorPosition (byte offset) to line/column
coordinates for accurate cursor rendering.
- Add debug logging in HandleKeyDown and HandleCursorMove.
223 lines
6.2 KiB
Go
223 lines
6.2 KiB
Go
package editor
|
|
|
|
import (
|
|
"log"
|
|
"sync"
|
|
|
|
"pad/internal/browser"
|
|
"pad/internal/io/pool"
|
|
"pad/internal/io/pool/mock"
|
|
"pad/internal/ui"
|
|
)
|
|
|
|
// ConfigEvent represents a window configuration change (resize, orientation).
|
|
// PixelWidth and PixelHeight are the raw pixel dimensions from Gio.
|
|
type ConfigEvent struct {
|
|
PixelWidth int
|
|
PixelHeight int
|
|
}
|
|
|
|
// ScaleEvent represents a metric change (HiDPI scale factor).
|
|
type ScaleEvent struct {
|
|
Scale float32
|
|
}
|
|
|
|
// ConfigUpdate is a common interface for all configuration updates.
|
|
// Both ConfigEvent and ScaleEvent implement this interface.
|
|
type ConfigUpdate interface {
|
|
apply(*State)
|
|
}
|
|
|
|
func (e ConfigEvent) apply(s *State) {
|
|
s.PixelWidth = e.PixelWidth
|
|
s.PixelHeight = e.PixelHeight
|
|
}
|
|
|
|
func (e ScaleEvent) apply(s *State) {
|
|
s.SetScale(e.Scale)
|
|
}
|
|
|
|
// 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
|
|
browserManager *browser.BrowserManager // Add this field
|
|
configChan chan ConfigUpdate
|
|
frameChan chan []ui.Element
|
|
inputChan chan []ui.InputEvent
|
|
lastLineYChan chan int // last line Y (Dp, sent as int) feedback from renderer
|
|
resultChan chan ResultEvent
|
|
searchQueryChan chan string // search text updates from main goroutine
|
|
openFileChan chan string // Added for asynchronous file loading
|
|
workerPool *pool.WorkerPool
|
|
mockFS *mock.FileSystem
|
|
mu sync.Mutex
|
|
done chan struct{}
|
|
}
|
|
|
|
// NewLogic creates a new Logic instance.
|
|
func NewLogic() *Logic {
|
|
state := NewState()
|
|
TheState = state
|
|
|
|
// Initialize mock filesystem with sample data
|
|
mockFS := mock.NewFileSystem()
|
|
populateMockFileSystem(mockFS)
|
|
|
|
// Initialize worker pool
|
|
wp := pool.NewWorkerPool(4)
|
|
wp.Start()
|
|
|
|
// Set browser initial path to mock root
|
|
state.Browser.CurrentPath = "/"
|
|
bm, _ := browser.NewBrowserManager(&state.Browser, wp, mockFS)
|
|
|
|
return &Logic{
|
|
state: state,
|
|
browserManager: bm, // Add to struct
|
|
configChan: make(chan ConfigUpdate),
|
|
frameChan: make(chan []ui.Element),
|
|
inputChan: make(chan []ui.InputEvent),
|
|
lastLineYChan: make(chan int),
|
|
resultChan: make(chan ResultEvent),
|
|
searchQueryChan: make(chan string),
|
|
openFileChan: make(chan string), // Initialized
|
|
workerPool: wp,
|
|
mockFS: mockFS,
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// ConfigChan returns the unified config channel for the logic goroutine.
|
|
// Accepts ConfigEvent (size) and ScaleEvent (scale factor).
|
|
func (l *Logic) ConfigChan() chan<- ConfigUpdate {
|
|
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<- []ui.InputEvent {
|
|
return l.inputChan
|
|
}
|
|
|
|
// DisplayLineChan returns the last line Y feedback channel.
|
|
func (l *Logic) DisplayLineChan() chan<- int {
|
|
return l.lastLineYChan
|
|
}
|
|
|
|
// ResultChan returns the result channel for the logic goroutine.
|
|
func (l *Logic) ResultChan() chan<- ResultEvent {
|
|
return l.resultChan
|
|
}
|
|
|
|
// SearchQueryChan returns the search query channel for the logic goroutine.
|
|
// The main goroutine sends updated search text here when it detects a change.
|
|
func (l *Logic) SearchQueryChan() chan<- string {
|
|
return l.searchQueryChan
|
|
}
|
|
|
|
// TheState is the global editor state, set once at startup.
|
|
var TheState *State
|
|
|
|
// Scale returns the current scale factor (pixels per DP).
|
|
func (l *Logic) Scale() float32 {
|
|
return l.state.Scale()
|
|
}
|
|
|
|
// Run runs the logic goroutine loop.
|
|
func (l *Logic) Run() {
|
|
// Dispatch initial directory index build on startup
|
|
log.Printf("Logic: Dispatching BuildIndexTask")
|
|
l.workerPool.Dispatch(pool.NewBuildIndexTask(l.state.Browser.CurrentPath, l.mockFS))
|
|
|
|
for {
|
|
select {
|
|
case <-l.done:
|
|
return
|
|
case update := <-l.configChan:
|
|
log.Printf("Logic: ConfigEvent")
|
|
update.apply(l.state)
|
|
l.frameChan <- l.state.layout(l.browserManager)
|
|
case y := <-l.lastLineYChan:
|
|
if ui.Dp(y) != l.state.LastLineY {
|
|
l.state.LastLineY = ui.Dp(y)
|
|
l.frameChan <- l.state.layout(l.browserManager)
|
|
}
|
|
case events := <-l.inputChan:
|
|
log.Printf("Logic: InputEvents")
|
|
for _, evt := range events {
|
|
evt.Handler(evt.Data)
|
|
}
|
|
l.frameChan <- l.state.layout(l.browserManager)
|
|
case query := <-l.searchQueryChan:
|
|
log.Printf("Logic: SearchQuery")
|
|
if query != l.state.Browser.Query {
|
|
l.state.Browser.Query = query
|
|
if l.state.page == BrowserPage {
|
|
browser.HandleSearch(&l.state.Browser, query)
|
|
}
|
|
}
|
|
l.frameChan <- l.state.layout(l.browserManager)
|
|
case path := <-l.openFileChan:
|
|
log.Printf("Logic: OpenFileChan %s", path)
|
|
// Dispatch ReadFileTask to worker pool
|
|
l.workerPool.Dispatch(pool.NewReadFileTask(path, l.mockFS))
|
|
case res := <-l.workerPool.ResultChan():
|
|
log.Printf("Logic: WorkerResult %s", res.TaskType)
|
|
l.handleWorkerResult(res)
|
|
case <-l.resultChan:
|
|
l.frameChan <- l.state.layout(l.browserManager)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleWorkerResult processes results from the worker pool.
|
|
func (l *Logic) handleWorkerResult(res pool.Result) {
|
|
if res.IsBrowserResult() {
|
|
l.browserManager.HandleResult(res)
|
|
} else if res.TaskType == pool.TypeReadFile {
|
|
log.Printf("Logic: TypeReadFile result success=%v", res.Success)
|
|
if res.Success {
|
|
if content, ok := res.Data.([]byte); ok {
|
|
l.state.Editor.Buffer = string(content)
|
|
}
|
|
}
|
|
}
|
|
l.frameChan <- l.state.layout(l.browserManager)
|
|
}
|
|
|
|
// applyBuildIndexResult applies a completed BuildIndexTask result to browser state.
|
|
func (l *Logic) applyBuildIndexResult(res pool.Result) {
|
|
// Delegated to browserManager
|
|
}
|
|
|
|
// applyLoadPagesResult applies a completed LoadPagesTask result to browser state.
|
|
func (l *Logic) applyLoadPagesResult(res pool.Result) {
|
|
// Delegated to browserManager
|
|
}
|
|
|
|
// applyReadDirResult applies a completed ReadDirTask result to browser state.
|
|
func (l *Logic) applyReadDirResult(res pool.Result) {
|
|
// Delegated to browserManager
|
|
}
|
|
|
|
// State returns the current state.
|
|
func (l *Logic) State() *State {
|
|
return l.state
|
|
}
|
|
|
|
// Done signals the logic goroutine to stop.
|
|
func (l *Logic) Done() {
|
|
close(l.done)
|
|
}
|