diff --git a/internal/control/cli/controller.go b/internal/control/cli/controller.go index d0e11788..e6a4e135 100644 --- a/internal/control/cli/controller.go +++ b/internal/control/cli/controller.go @@ -472,24 +472,26 @@ func NewController( controller.data.TaskEditor = nil return } - taskEditorPane, err := controller.data.TaskEditor.GetPane( - ui.NewConstrainedRenderer(renderer, func() (x, y, w, h int) { - screenWidth, screenHeight := screenSize() - taskEditorBoxWidth := int(math.Min(80, float64(screenWidth))) - taskEditorBoxHeight := int(math.Min(20, float64(screenHeight))) - return (screenWidth / 2) - (taskEditorBoxWidth / 2), (screenHeight / 2) - (taskEditorBoxHeight / 2), taskEditorBoxWidth, taskEditorBoxHeight - }), + + taskEditorRenderer := ui.NewConstrainedRenderer(renderer, func() (x, y, w, h int) { + screenWidth, screenHeight := screenSize() + taskEditorBoxWidth := int(math.Min(80, float64(screenWidth))) + taskEditorBoxHeight := int(math.Min(20, float64(screenHeight))) + return (screenWidth / 2) - (taskEditorBoxWidth / 2), (screenHeight / 2) - (taskEditorBoxHeight / 2), taskEditorBoxWidth, taskEditorBoxHeight + }) + + taskEditorPane, err := panes.NewCompositeEditorPane( + taskEditorRenderer, + cursorWrangler, func() bool { return true }, inputConfig, stylesheet, - cursorWrangler, + controller.data.TaskEditor, ) if err != nil { - log.Error().Err(err).Msgf("could not construct task editor pane") - controller.data.TaskEditor = nil - return + log.Fatal().Err(err).Msg("could not construct task editor pane (this is likely a serious programming error / omission)") } - log.Info().Str("info", taskEditorPane.(*panes.CompositeEditorPane).GetDebugInfo()).Msg("here is the debug info for the task editor pane") + controller.rootPane.PushSubpane(taskEditorPane) taskEditorDone := make(chan struct{}) controller.data.TaskEditor.AddQuitCallback(func() { diff --git a/internal/control/edit/editor.go b/internal/control/edit/editor.go index 0dffaa04..9093b178 100644 --- a/internal/control/edit/editor.go +++ b/internal/control/edit/editor.go @@ -2,12 +2,6 @@ // user). package edit -import ( - "github.com/ja-he/dayplan/internal/input" - "github.com/ja-he/dayplan/internal/styling" - "github.com/ja-he/dayplan/internal/ui" -) - // Editor is an interface for editing of objects (by the user). type Editor interface { IsActiveAndFocussed() (bool, bool) @@ -15,7 +9,6 @@ type Editor interface { GetName() string GetType() string - GetSummary() SummaryEntry // Write the state of the editor. Write() @@ -25,18 +18,18 @@ type Editor interface { // AddQuitCallback adds a callback that is called when the editor is quit. AddQuitCallback(func()) - - // GetPane returns a pane that represents this editor. - GetPane( - renderer ui.ConstrainedRenderer, - visible func() bool, - inputConfig input.InputConfig, - stylesheet styling.Stylesheet, - cursorController ui.CursorLocationRequestHandler, - ) (ui.Pane, error) } -type SummaryEntry struct { - Representation any - Represents Editor +// View is an interface for viewing of objects (by the user). +type View interface { + IsActiveAndFocussed() (bool, bool) + + GetName() string + + // GetType reutrns the type of the view. + // + // Based on this information, a cast to a relevant type can be done. + // E.g., when GetType() returns "string", the view can be cast to a + // StringEditorView. + GetType() string } diff --git a/internal/control/edit/editors/composite_editor.go b/internal/control/edit/editors/composite_editor.go index a9b6bf76..1cbe4e44 100644 --- a/internal/control/edit/editors/composite_editor.go +++ b/internal/control/edit/editors/composite_editor.go @@ -1,3 +1,4 @@ +// Package editors contains the editors for the different data types. package editors import ( @@ -12,9 +13,6 @@ import ( "github.com/ja-he/dayplan/internal/input" "github.com/ja-he/dayplan/internal/input/processors" "github.com/ja-he/dayplan/internal/model" - "github.com/ja-he/dayplan/internal/styling" - "github.com/ja-he/dayplan/internal/ui" - "github.com/ja-he/dayplan/internal/ui/panes" ) // Composite implements Editor @@ -207,58 +205,8 @@ func (e *Composite) Quit() { } } -// GetPane constructs a pane for this composite editor (including all subeditors). -func (e *Composite) GetPane( - renderer ui.ConstrainedRenderer, - visible func() bool, - inputConfig input.InputConfig, - stylesheet styling.Stylesheet, - cursorController ui.CursorLocationRequestHandler, -) (ui.Pane, error) { - subpanes := []ui.Pane{} - - // TODO: this needs to compute an enriched version of the editor tree - editorSummary := e.GetSummary() - minX, minY, maxWidth, maxHeight := renderer.Dimensions() - uiBoxModel, err := translateToUIBoxModel(editorSummary, minX, minY, maxWidth, maxHeight) - if err != nil { - return nil, fmt.Errorf("error translating editor summary to UI box model (%s)", err.Error()) - } - log.Debug().Msgf("have UI box model: %s", uiBoxModel.String()) - - for _, child := range uiBoxModel.Children { - childX, childY, childW, childH := child.X, child.Y, child.W, child.H - subRenderer := ui.NewConstrainedRenderer(renderer, func() (int, int, int, int) { return childX, childY, childW, childH }) - subeditorPane, err := child.Represents.GetPane( - subRenderer, - visible, - inputConfig, - stylesheet, - cursorController, - ) - if err != nil { - return nil, fmt.Errorf("error constructing subpane of '%s' for subeditor '%s' (%s)", e.name, child.Represents.GetName(), err.Error()) - } - subpanes = append(subpanes, subeditorPane) - } - - inputProcessor, err := e.createInputProcessor(inputConfig) - if err != nil { - return nil, fmt.Errorf("could not construct input processor (%s)", err.Error()) - } - return panes.NewCompositeEditorPane( - renderer, - visible, - inputProcessor, - stylesheet, - subpanes, - func() int { return e.activeFieldIndex }, - func() bool { return e.inField }, - e, - ), nil -} - -func (e *Composite) createInputProcessor(cfg input.InputConfig) (input.ModalInputProcessor, error) { +// CreateInputProcessor creates an input processor for the editor. +func (e *Composite) CreateInputProcessor(cfg input.InputConfig) (input.ModalInputProcessor, error) { actionspecToFunc := map[input.Actionspec]func(){ "next-field": e.SwitchToNextField, "prev-field": e.SwitchToPrevField, @@ -282,68 +230,12 @@ func (e *Composite) createInputProcessor(cfg input.InputConfig) (input.ModalInpu return processors.NewModalInputProcessor(inputTree), nil } -func (e *Composite) IsActiveAndFocussed() (bool, bool) { return e.activeAndFocussedFunc() } - -func (e *Composite) GetSummary() edit.SummaryEntry { - - result := edit.SummaryEntry{ - Representation: []edit.SummaryEntry{}, - Represents: e, - } - for _, subeditor := range e.fields { - log.Debug().Msgf("constructing subpane of '%s' for subeditor '%s'", e.name, subeditor.GetName()) - result.Representation = append(result.Representation.([]edit.SummaryEntry), subeditor.GetSummary()) - } - - return result -} - -func translateToUIBoxModel(summary edit.SummaryEntry, minX, minY, maxWidth, maxHeight int) (ui.BoxRepresentation[edit.Editor], error) { - - switch repr := summary.Representation.(type) { +func (e *Composite) GetActiveFieldIndex() int { return e.activeFieldIndex } +func (e *Composite) IsInField() bool { return e.inField } - // a slice indicates a composite - case []edit.SummaryEntry: - var children []ui.BoxRepresentation[edit.Editor] - computedHeight := 1 - rollingY := minY + 1 - for _, child := range repr { - childBoxRepresentation, err := translateToUIBoxModel(child, minX+1, rollingY, maxWidth-2, maxHeight-2) - if err != nil { - return ui.BoxRepresentation[edit.Editor]{}, fmt.Errorf("error translating child '%s' (%s)", child.Represents.GetName(), err.Error()) - } - rollingY += childBoxRepresentation.H + 1 - children = append(children, childBoxRepresentation) - computedHeight += childBoxRepresentation.H + 1 - } - return ui.BoxRepresentation[edit.Editor]{ - X: minX, - Y: minY, - W: maxWidth, - H: computedHeight, - Represents: summary.Represents, - Children: children, - }, nil - - // a string indicates a leaf, i.e., a concrete editor rather than a composite - case string: - switch repr { - case "string": - return ui.BoxRepresentation[edit.Editor]{ - X: minX, - Y: minY, - W: maxWidth, - H: 1, - Represents: summary.Represents, - Children: nil, - }, nil - default: - return ui.BoxRepresentation[edit.Editor]{}, fmt.Errorf("unknown editor identification value '%s'", repr) - } - - default: - return ui.BoxRepresentation[edit.Editor]{}, fmt.Errorf("for editor '%s' have unknown type '%t'", summary.Represents.GetName(), summary.Representation) - - } +func (e *Composite) IsActiveAndFocussed() (bool, bool) { return e.activeAndFocussedFunc() } +// GetFields returns the subeditors of this composite editor. +func (e *Composite) GetFields() []edit.Editor { + return e.fields } diff --git a/internal/control/edit/editors/string_editor.go b/internal/control/edit/editors/string_editor.go index ec7e0ffb..acb4bcaa 100644 --- a/internal/control/edit/editors/string_editor.go +++ b/internal/control/edit/editors/string_editor.go @@ -5,12 +5,8 @@ import ( "strconv" "github.com/ja-he/dayplan/internal/control/action" - "github.com/ja-he/dayplan/internal/control/edit" "github.com/ja-he/dayplan/internal/input" "github.com/ja-he/dayplan/internal/input/processors" - "github.com/ja-he/dayplan/internal/styling" - "github.com/ja-he/dayplan/internal/ui" - "github.com/ja-he/dayplan/internal/ui/panes" "github.com/rs/zerolog/log" ) @@ -245,30 +241,8 @@ func (e *StringEditor) AddQuitCallback(f func()) { } } -// GetPane returns a UI pane representing the editor. -func (e *StringEditor) GetPane( - renderer ui.ConstrainedRenderer, - visible func() bool, - inputConfig input.InputConfig, - stylesheet styling.Stylesheet, - cursorController ui.CursorLocationRequestHandler, -) (ui.Pane, error) { - inputProcessor, err := e.createInputProcessor(inputConfig) - if err != nil { - return nil, err - } - p := panes.NewStringEditorPane( - renderer, - visible, - inputProcessor, - e, - stylesheet, - cursorController, - ) - return p, nil -} - -func (e *StringEditor) createInputProcessor(cfg input.InputConfig) (input.ModalInputProcessor, error) { +// CreateInputProcessor creates an input processor for the editor. +func (e *StringEditor) CreateInputProcessor(cfg input.InputConfig) (input.ModalInputProcessor, error) { var enterInsertMode func() var exitInsertMode func() @@ -325,10 +299,3 @@ func (e *StringEditor) createInputProcessor(cfg input.InputConfig) (input.ModalI return p, nil } - -func (e *StringEditor) GetSummary() edit.SummaryEntry { - return edit.SummaryEntry{ - Representation: "string", - Represents: e, - } -} diff --git a/internal/control/edit/views/composite_editor.go b/internal/control/edit/views/composite_editor.go deleted file mode 100644 index 91ed1359..00000000 --- a/internal/control/edit/views/composite_editor.go +++ /dev/null @@ -1,12 +0,0 @@ -package views - -// StringEditorView allows inspection of a string editor. -type CompositeEditorView interface { - - // IsActive signals whether THIS is active. (SHOULD BE MOVED TO A MORE GENERIC INTERFACE) - IsActiveAndFocussed() (bool, bool) - - GetName() string - - // TODO: more -} diff --git a/internal/control/edit/views/string_editor.go b/internal/control/edit/views/string_editor.go deleted file mode 100644 index d09de597..00000000 --- a/internal/control/edit/views/string_editor.go +++ /dev/null @@ -1,24 +0,0 @@ -package views - -import "github.com/ja-he/dayplan/internal/input" - -// StringEditorView allows inspection of a string editor. -type StringEditorView interface { - - // IsActive signals whether THIS is active. (SHOULD BE MOVED TO A MORE GENERIC INTERFACE) - IsActiveAndFocussed() (bool, bool) - - // GetMode returns the current mode of the editor. - GetMode() input.TextEditMode - - // GetCursorPos returns the current cursor position in the string, 0 being - // the first character. - GetCursorPos() int - - // GetContent returns the current (edited) contents. - GetContent() string - - GetName() string - - // TODO: more -} diff --git a/internal/ui/panes/composite_editor_ui_pane.go b/internal/ui/panes/composite_editor_ui_pane.go index 6c172953..bc49726f 100644 --- a/internal/ui/panes/composite_editor_ui_pane.go +++ b/internal/ui/panes/composite_editor_ui_pane.go @@ -7,7 +7,8 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/ja-he/dayplan/internal/control/edit/views" + "github.com/ja-he/dayplan/internal/control/edit" + "github.com/ja-he/dayplan/internal/control/edit/editors" "github.com/ja-he/dayplan/internal/input" "github.com/ja-he/dayplan/internal/styling" "github.com/ja-he/dayplan/internal/ui" @@ -20,7 +21,7 @@ type CompositeEditorPane struct { getFocussedIndex func() int isInField func() bool - view views.CompositeEditorView + e *editors.Composite subpanes []ui.Pane bgoffs int @@ -35,14 +36,14 @@ func (p *CompositeEditorPane) Draw() { // draw background style := p.Stylesheet.Editor.DarkenedBG(p.bgoffs) - active, focussed := p.view.IsActiveAndFocussed() + active, focussed := p.e.IsActiveAndFocussed() if active { style = style.DarkenedBG(20) } else if focussed { style = style.DarkenedBG(40) } p.Renderer.DrawBox(x, y, w, h, style) - p.Renderer.DrawText(x, y, w, 1, p.Stylesheet.Editor.DarkenedFG(20), p.view.GetName()) + p.Renderer.DrawText(x, y, w, 1, p.Stylesheet.Editor.DarkenedFG(20), p.e.GetName()) // draw all subpanes for _, subpane := range p.subpanes { @@ -100,14 +101,46 @@ func (p *CompositeEditorPane) GetPositionInfo(_, _ int) ui.PositionInfo { return // NewCompositeEditorPane creates a new CompositeEditorPane. func NewCompositeEditorPane( renderer ui.ConstrainedRenderer, + cursorController ui.CursorLocationRequestHandler, visible func() bool, - inputProcessor input.ModalInputProcessor, + inputConfig input.InputConfig, stylesheet styling.Stylesheet, - subEditors []ui.Pane, - getFocussedIndex func() int, - isInField func() bool, - view views.CompositeEditorView, -) *CompositeEditorPane { + e *editors.Composite, +) (*CompositeEditorPane, error) { + + subpanes := []ui.Pane{} + + minX, minY, maxWidth, maxHeight := renderer.Dimensions() + uiBoxModel, err := translateEditorsCompositeToTUI(e, minX, minY, maxWidth, maxHeight) + if err != nil { + return nil, fmt.Errorf("error translating editor summary to UI box model (%s)", err.Error()) + } + log.Debug().Msgf("have UI box model: %s", uiBoxModel.String()) + + for _, child := range uiBoxModel.Children { + childX, childY, childW, childH := child.X, child.Y, child.W, child.H + subRenderer := ui.NewConstrainedRenderer(renderer, func() (int, int, int, int) { return childX, childY, childW, childH }) + var subeditorPane ui.Pane + var err error + switch child := child.Represents.(type) { + case *editors.StringEditor: + subeditorPane, err = NewStringEditorPane(subRenderer, cursorController, visible, stylesheet, inputConfig, child) + case *editors.Composite: + subeditorPane, err = NewCompositeEditorPane(subRenderer, cursorController, visible, inputConfig, stylesheet, child) + default: + err = fmt.Errorf("unhandled subeditor type '%T' (forgot to handle case)", child) + } + if err != nil { + return nil, fmt.Errorf("error constructing subpane of '%s' for subeditor '%s' (%s)", e.GetName(), child.Represents.GetName(), err.Error()) + } + subpanes = append(subpanes, subeditorPane) + } + + inputProcessor, err := e.CreateInputProcessor(inputConfig) + if err != nil { + return nil, fmt.Errorf("could not construct input processor (%s)", err.Error()) + } + return &CompositeEditorPane{ LeafPane: ui.LeafPane{ BasePane: ui.BasePane{ @@ -119,13 +152,13 @@ func NewCompositeEditorPane( Dims: renderer.Dimensions, Stylesheet: stylesheet, }, - subpanes: subEditors, - getFocussedIndex: getFocussedIndex, - isInField: isInField, + subpanes: subpanes, + getFocussedIndex: e.GetActiveFieldIndex, + isInField: e.IsInField, log: log.With().Str("source", "composite-pane").Logger(), bgoffs: 10 + rand.Intn(20), - view: view, - } + e: e, + }, nil } // GetHelp returns the input help map for this composite pane. @@ -152,20 +185,51 @@ func (p *CompositeEditorPane) GetHelp() input.Help { return result } -func (p *CompositeEditorPane) GetDebugInfo() string { - x, y, w, h := p.Dimensions() - info := fmt.Sprintf("[ +%d+%d:%dx%d ", x, y, w, h) - for _, subpane := range p.subpanes { - switch sp := subpane.(type) { - case *CompositeEditorPane: - info += sp.GetDebugInfo() - case *StringEditorPane: - x, y, w, h := sp.Dimensions() - info += fmt.Sprintf("( %d+%d:%dx%d )", x, y, w, h) - default: - info += fmt.Sprintf("", subpane) +func translateEditorsEditorToTUI(e edit.Editor, minX, minY, maxWidth, maxHeight int) (ui.BoxRepresentation[edit.Editor], error) { + + switch e := e.(type) { + + case *editors.Composite: + return translateEditorsCompositeToTUI(e, minX, minY, maxWidth, maxHeight) + + case *editors.StringEditor: + return ui.BoxRepresentation[edit.Editor]{ + X: minX, + Y: minY, + W: maxWidth, + H: 1, + Represents: e, + Children: nil, + }, nil + + default: + return ui.BoxRepresentation[edit.Editor]{}, fmt.Errorf("unhandled editor type '%T' (forgot to handle case)", e) + + } + +} + +func translateEditorsCompositeToTUI(e *editors.Composite, minX, minY, maxWidth, maxHeight int) (ui.BoxRepresentation[edit.Editor], error) { + + var children []ui.BoxRepresentation[edit.Editor] + computedHeight := 1 + rollingY := minY + 1 + for _, child := range e.GetFields() { + childBoxRepresentation, err := translateEditorsEditorToTUI(child, minX+1, rollingY, maxWidth-2, maxHeight-2) + if err != nil { + return ui.BoxRepresentation[edit.Editor]{}, fmt.Errorf("error translating child '%s' (%s)", child.GetName(), err.Error()) } + rollingY += childBoxRepresentation.H + 1 + children = append(children, childBoxRepresentation) + computedHeight += childBoxRepresentation.H + 1 } - info += "]" - return info + return ui.BoxRepresentation[edit.Editor]{ + X: minX, + Y: minY, + W: maxWidth, + H: computedHeight, + Represents: e, + Children: children, + }, nil + } diff --git a/internal/ui/panes/string_editor_ui_pane.go b/internal/ui/panes/string_editor_ui_pane.go index 11eaa7c5..7c9f5dde 100644 --- a/internal/ui/panes/string_editor_ui_pane.go +++ b/internal/ui/panes/string_editor_ui_pane.go @@ -1,10 +1,12 @@ package panes import ( + "fmt" + "github.com/google/uuid" "github.com/rs/zerolog/log" - "github.com/ja-he/dayplan/internal/control/edit/views" + "github.com/ja-he/dayplan/internal/control/edit/editors" "github.com/ja-he/dayplan/internal/input" "github.com/ja-he/dayplan/internal/styling" "github.com/ja-he/dayplan/internal/ui" @@ -14,7 +16,7 @@ import ( type StringEditorPane struct { ui.LeafPane - view views.StringEditorView + e *editors.StringEditor cursorController ui.CursorLocationRequestHandler @@ -27,7 +29,7 @@ func (p *StringEditorPane) Draw() { x, y, w, h := p.Dims() baseBGStyle := p.Stylesheet.Editor - active, focussed := p.view.IsActiveAndFocussed() + active, focussed := p.e.IsActiveAndFocussed() if active { baseBGStyle = baseBGStyle.DarkenedBG(10) } else if focussed { @@ -39,10 +41,10 @@ func (p *StringEditorPane) Draw() { padding := 1 p.Renderer.DrawBox(x, y, w, h, baseBGStyle) - p.Renderer.DrawText(x+padding, y, nameWidth, h, baseBGStyle.Italicized(), p.view.GetName()) + p.Renderer.DrawText(x+padding, y, nameWidth, h, baseBGStyle.Italicized(), p.e.GetName()) if focussed { - switch p.view.GetMode() { + switch p.e.GetMode() { case input.TextEditModeInsert: p.Renderer.DrawText(x+padding+nameWidth+padding, y, modeWidth, h, baseBGStyle.DarkenedFG(30).Invert(), "(ins)") case input.TextEditModeNormal: @@ -53,10 +55,10 @@ func (p *StringEditorPane) Draw() { } contentXOffset := padding + nameWidth + padding + modeWidth + padding - p.Renderer.DrawText(x+contentXOffset, y, w-contentXOffset+padding, h, baseBGStyle.DarkenedBG(20), p.view.GetContent()) + p.Renderer.DrawText(x+contentXOffset, y, w-contentXOffset+padding, h, baseBGStyle.DarkenedBG(20), p.e.GetContent()) if focussed { - cursorX, cursorY := x+contentXOffset+(p.view.GetCursorPos()), y + cursorX, cursorY := x+contentXOffset+(p.e.GetCursorPos()), y p.cursorController.Put(ui.CursorLocation{X: cursorX, Y: cursorY}, p.idStr) } else { p.cursorController.Delete(p.idStr) @@ -76,7 +78,7 @@ func (p *StringEditorPane) GetPositionInfo(_, _ int) ui.PositionInfo { return ni // ProcessInput attempts to process the provided input. func (p *StringEditorPane) ProcessInput(k input.Key) bool { - active, _ := p.view.IsActiveAndFocussed() + active, _ := p.e.IsActiveAndFocussed() if !active { log.Warn().Msgf("string editor pane asked to process input despite view reporting not active; likely logic error") } @@ -86,12 +88,17 @@ func (p *StringEditorPane) ProcessInput(k input.Key) bool { // NewStringEditorPane creates a new StringEditorPane. func NewStringEditorPane( renderer ui.ConstrainedRenderer, + cursorController ui.CursorLocationRequestHandler, visible func() bool, - inputProcessor input.ModalInputProcessor, - view views.StringEditorView, stylesheet styling.Stylesheet, - cursorController ui.CursorLocationRequestHandler, -) *StringEditorPane { + inputConfig input.InputConfig, + e *editors.StringEditor, +) (*StringEditorPane, error) { + inputProcessor, err := e.CreateInputProcessor(inputConfig) + if err != nil { + return nil, fmt.Errorf("could not construct normal mode input tree (%s)", err.Error()) + } + return &StringEditorPane{ LeafPane: ui.LeafPane{ BasePane: ui.BasePane{ @@ -103,8 +110,8 @@ func NewStringEditorPane( Dims: renderer.Dimensions, Stylesheet: stylesheet, }, - view: view, + e: e, cursorController: cursorController, idStr: "string-editor-pane-" + uuid.Must(uuid.NewRandom()).String(), - } + }, nil }