WIP components (as built)
This commit is contained in:
96 files changed
+6468
-16
No files matched your search
@@ -0,0 +1,197 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// RenameInlineOptions configure a RenameInline.
|
||||
type RenameInlineOptions struct {
|
||||
// Validate returns an error message, or empty for acceptance.
|
||||
Validate func(name string) string
|
||||
// Folder selects the whole name: folders have no extension.
|
||||
Folder bool
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// RenameInlineResult is a RenameInline and its outcome in this frame.
|
||||
type RenameInlineResult struct {
|
||||
Element *ui.Element
|
||||
// Renamed reports a saved name; Canceled reports an abandoned edit.
|
||||
Renamed, Canceled bool
|
||||
}
|
||||
|
||||
// renameState is one editing session.
|
||||
type renameState struct {
|
||||
active bool
|
||||
draft string
|
||||
// gen rebuilds the editor with its caret at the end.
|
||||
gen int
|
||||
// tail appends text next frame behind the caret.
|
||||
tail string
|
||||
shown bool
|
||||
stemSel bool
|
||||
focused bool
|
||||
err string
|
||||
folder bool
|
||||
}
|
||||
|
||||
// renameStem is the part of name before its extension.
|
||||
func renameStem(name string, folder bool) string {
|
||||
ext := path.Ext(name)
|
||||
if folder || ext == name || ext == "" {
|
||||
return name
|
||||
}
|
||||
return strings.TrimSuffix(name, ext)
|
||||
}
|
||||
|
||||
// RenameInline edits names with validation and initial stem replacement.
|
||||
// Enter validates; Escape cancels; blur confirms valid names.
|
||||
func RenameInline(c *ui.Context, name *string, editing *bool, opts RenameInlineOptions) RenameInlineResult {
|
||||
if name == nil || editing == nil {
|
||||
panic("mujica: RenameInline needs a name and editing")
|
||||
}
|
||||
var res RenameInlineResult
|
||||
res.Element = ui.Row(c).MinWidth(0).MinHeight(theme.MinHitSize).AlignItems(ui.Center).Disabled(opts.Disabled)
|
||||
st := ui.Local(res.Element, "rename_inline", func() renameState { return renameState{} })
|
||||
if res.Element.IsDisabled() && *editing {
|
||||
*editing = false
|
||||
res.Canceled = st.active
|
||||
st.active = false
|
||||
}
|
||||
res.Element.Children(func() {
|
||||
if !*editing {
|
||||
st.active = false
|
||||
t := ui.Text(c, *name).SingleLine().MinWidth(0)
|
||||
if t.DoubleClicked() && !res.Element.IsDisabled() {
|
||||
*editing = true
|
||||
c.Invalidate()
|
||||
}
|
||||
return
|
||||
}
|
||||
stem := renameStem(*name, opts.Folder)
|
||||
if !st.active {
|
||||
// Start with the stem and its caret at the end.
|
||||
*st = renameState{active: true, draft: stem, gen: st.gen + 1, tail: (*name)[len(stem):], stemSel: true, folder: opts.Folder}
|
||||
slog.Debug("mujica: rename_inline start")
|
||||
}
|
||||
switch {
|
||||
case st.tail != "" && st.shown:
|
||||
st.draft, st.tail, st.shown = st.draft+st.tail, "", false
|
||||
case st.tail != "":
|
||||
// Show the head for one frame to place its caret.
|
||||
st.shown = true
|
||||
c.Invalidate()
|
||||
}
|
||||
finish := func(ok bool) {
|
||||
if ok {
|
||||
if why := renameCheck(c, st.draft, opts.Validate); why != "" {
|
||||
st.err = why
|
||||
return
|
||||
}
|
||||
res.Renamed = st.draft != *name
|
||||
*name = st.draft
|
||||
} else {
|
||||
res.Canceled = true
|
||||
}
|
||||
*editing, st.active = false, false
|
||||
slog.Debug("mujica: rename_inline end", "renamed", res.Renamed)
|
||||
}
|
||||
ui.Box(c).Key(st.gen).Grow(1).MinWidth(0).Children(func() {
|
||||
in := TextInput(c, &st.draft, TextInputOptions{Label: msg(c, "rename_inline.label"), Error: st.err})
|
||||
in.Element.FillWidth()
|
||||
in.Input.AutoFocus()
|
||||
if in.Changed() {
|
||||
st.err = ""
|
||||
renameTyped(st, *name, opts.Folder)
|
||||
}
|
||||
renameSelection(c, in.Input, st)
|
||||
switch {
|
||||
case in.Input.Shortcut(0, ui.KeyEscape):
|
||||
finish(false)
|
||||
case in.Submitted():
|
||||
finish(true)
|
||||
case in.Input.Focused():
|
||||
st.focused = true
|
||||
case st.focused && st.tail == "":
|
||||
// Confirm valid names when focus leaves.
|
||||
finish(renameCheck(c, st.draft, opts.Validate) == "")
|
||||
}
|
||||
})
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
func renameCheck(c *ui.Context, name string, validate func(string) string) string {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return msg(c, "rename_inline.empty")
|
||||
}
|
||||
if validate != nil {
|
||||
return validate(name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// renameSelection marks the stem for replacement on initial input.
|
||||
func renameSelection(c *ui.Context, in *ui.Element, st *renameState) {
|
||||
k := Tokens(c)
|
||||
stem := renameStem(st.draft, st.folder)
|
||||
if st.stemSel && st.tail == "" {
|
||||
w, _ := c.MeasureText(0, ui.Span{Text: stem, Size: FontSize(c, theme.BodySize)})
|
||||
in.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
p.Fill(ui.Rect{X: r.X, Y: r.Y + 2, W: w, H: r.H - 4}, k.Selection, 2)
|
||||
})
|
||||
}
|
||||
in.HandleInput(func(ev ui.InputEvent) bool {
|
||||
if !st.stemSel || in.IsDisabled() {
|
||||
return false
|
||||
}
|
||||
switch {
|
||||
case ev.Kind == ui.InputKeyDown && ev.Mods == 0:
|
||||
switch ev.Key {
|
||||
case ui.KeyBackspace, ui.KeyDelete:
|
||||
renameReplace(st, "")
|
||||
return true
|
||||
case ui.KeyLeft, ui.KeyHome, ui.KeyUp:
|
||||
// An empty head clamps the caret to the start.
|
||||
st.draft, st.tail, st.stemSel, st.shown = "", st.draft, false, false
|
||||
return true
|
||||
case ui.KeyRight:
|
||||
st.stemSel = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
if ev.Kind == ui.InputPointerDown || ev.Kind == ui.InputKeyDown &&
|
||||
(ev.Key == ui.KeyLeft || ev.Key == ui.KeyRight || ev.Key == ui.KeyHome || ev.Key == ui.KeyEnd || ev.Key == ui.KeyUp || ev.Key == ui.KeyDown || ev.Key == ui.KeyA && ev.Mods&ui.Cmd != 0) {
|
||||
st.stemSel = false
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// renameReplace substitutes the stem and rebuilds the caret.
|
||||
func renameReplace(st *renameState, text string) {
|
||||
ext := st.draft[len(renameStem(st.draft, st.folder)):]
|
||||
st.draft, st.tail, st.gen, st.stemSel, st.shown = text, ext, st.gen+1, false, false
|
||||
}
|
||||
|
||||
// renameTyped replaces the stem with newly inserted text.
|
||||
func renameTyped(st *renameState, name string, folder bool) {
|
||||
if !st.stemSel {
|
||||
return
|
||||
}
|
||||
stem := renameStem(name, folder)
|
||||
ext := name[len(stem):]
|
||||
st.stemSel = false
|
||||
if strings.HasPrefix(st.draft, stem) && strings.HasSuffix(st.draft, ext) && len(st.draft) >= len(name) {
|
||||
typed := st.draft[len(stem) : len(st.draft)-len(ext)]
|
||||
st.draft = stem + ext
|
||||
renameReplace(st, typed)
|
||||
slog.Debug("mujica: rename_inline replace stem")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user