diff --git a/_demo/demo.go b/_demo/demo.go index 820f35d..891214a 100644 --- a/_demo/demo.go +++ b/_demo/demo.go @@ -30,6 +30,12 @@ func id(fn func (*nucular.Window)) func () func(*nucular.Window) { } } +func saveFnFor(i byte) func() []byte { + return func() []byte { + return []byte{ i } + } +} + type Demo struct { Name string Title string @@ -70,6 +76,16 @@ var demos = []Demo{ } }, { "nestedmenu", "Nested menu demo", 0, id(nestedMenu) }, { "list", "List", nucular.WindowNoScrollbar, id(listDemo) }, + { "saverestore", "Save / Restore", nucular.WindowNoScrollbar, nil }, +} + +func init() { + for i := range demos { + if demos[i].Name == "saverestore" { + demos[i].UpdateFn = saveRestoreDemo + return + } + } } var Wnd nucular.MasterWindow @@ -98,11 +114,11 @@ func main() { switch whichdemo { case "multi", "": Wnd = nucular.NewMasterWindow(0, "Multiwindow Demo", func(w *nucular.Window) {}) - Wnd.PopupOpen("Multiwindow Demo", nucular.WindowTitle|nucular.WindowBorder | nucular.WindowMovable | nucular.WindowScalable|nucular.WindowNonmodal, rect.Rect{ 0, 0, 400, 300 }, true, multiDemo) + Wnd.PopupOpenPersistent("Multiwindow Demo", nucular.WindowTitle|nucular.WindowBorder | nucular.WindowMovable | nucular.WindowScalable|nucular.WindowNonmodal, rect.Rect{ 0, 0, 400, 300 }, true, multiDemo, saveFnFor('m')) default: for i := range demos { if demos[i].Name == whichdemo { - Wnd = nucular.NewMasterWindow(demos[i].Flags, demos[i].Title, demos[i].UpdateFn()) + Wnd = nucular.NewMasterWindow(demos[i].Flags, demos[i].Title, demos[i].UpdateFn()) break } } @@ -337,7 +353,47 @@ func multiDemo(w *nucular.Window) { w.Row(30).Static(100, 100, 100) for i := range demos { if w.ButtonText(demos[i].Name) { - w.Master().PopupOpen(demos[i].Title, nucular.WindowDefaultFlags|nucular.WindowNonmodal | demos[i].Flags, rect.Rect{0, 0, 200, 200}, true, demos[i].UpdateFn()) + w.Master().PopupOpenPersistent(demos[i].Title, nucular.WindowDefaultFlags|nucular.WindowNonmodal | demos[i].Flags, rect.Rect{0, 0, 200, 200}, true, demos[i].UpdateFn(), saveFnFor(byte(i))) + } + } +} + +func restoreFn(data []byte, openWndFn nucular.OpenWindowFn) error { + if data[0] == 'm' { + openWndFn("Multiwindow Demo", nucular.WindowTitle|nucular.WindowBorder | nucular.WindowMovable | nucular.WindowScalable|nucular.WindowNonmodal, multiDemo, saveFnFor('m')) + return nil + } + i := data[0] + openWndFn(demos[i].Title, nucular.WindowDefaultFlags|nucular.WindowNonmodal | demos[i].Flags, demos[i].UpdateFn(), saveFnFor(byte(i))) + return nil +} + +func saveRestoreDemo() func(w *nucular.Window) { + var saveed nucular.TextEditor + saveed.Flags = nucular.EditSelectable | nucular.EditClipboard + wd, _ := os.Getwd() + saveed.Buffer = []rune(fmt.Sprintf("%s/save.sav", wd)) + return func(w *nucular.Window) { + w.Row(30).Static(0) + saveed.Edit(w) + w.Row(30).Static(0, 100, 100) + w.Spacing(1) + if w.ButtonText("Save") { + s, err := w.Master().Save() + if err != nil { + fmt.Fprintf(os.Stderr, "Error saving layout: %v", err) + } + ioutil.WriteFile(string(saveed.Buffer), s, 0666) + w.Close() + } + if w.ButtonText("Restore") { + data, err := ioutil.ReadFile(string(saveed.Buffer)) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading save file: %v", err) + } else { + w.Master().Restore(data, restoreFn) + } + w.Close() } } } diff --git a/context.go b/context.go index d654a83..2699ed6 100644 --- a/context.go +++ b/context.go @@ -9,6 +9,7 @@ import ( "time" "github.com/aarzilli/nucular/command" + "github.com/aarzilli/nucular/rect" nstyle "github.com/aarzilli/nucular/style" "github.com/golang/freetype/raster" @@ -23,17 +24,27 @@ type context struct { Input Input Style nstyle.Style Windows []*Window + DockedWindows dockedTree changed int32 activateEditor *TextEditor cmds []command.Command trashFrame bool autopos image.Point + + dockedWindowFocus int + dockedCnt int } func contextAllCommands(ctx *context) { ctx.cmds = ctx.cmds[:0] - for _, w := range ctx.Windows { + for i, w := range ctx.Windows { ctx.cmds = append(ctx.cmds, w.cmds.Commands...) + if i == 0 { + ctx.DockedWindows.Walk(func(w *Window) *Window { + ctx.cmds = append(ctx.cmds, w.cmds.Commands...) + return w + }) + } } return } @@ -42,7 +53,7 @@ func (ctx *context) setupMasterWindow(layout *panel, updatefn UpdateFn) { ctx.Windows = append(ctx.Windows, createWindow(ctx, "")) ctx.Windows[0].idx = 0 ctx.Windows[0].layout = layout - ctx.Windows[0].flags = layout.Flags + ctx.Windows[0].flags = layout.Flags | WindowNonmodal ctx.Windows[0].cmds.UseClipping = true ctx.Windows[0].updateFn = updatefn } @@ -55,23 +66,12 @@ func (ctx *context) Update() { } ctx.Restack() for i := 0; i < len(ctx.Windows); i++ { // this must not use range or tooltips won't work - win := ctx.Windows[i] - if win.updateFn != nil { - win.specialPanelBegin() - win.updateFn(win) - } - - if !win.began { - win.close = true - continue - } - - if win.title == tooltipWindowTitle { - win.close = true - } - - if win.flags&windowPopup != 0 { - panelEnd(ctx, win) + ctx.updateWindow(ctx.Windows[i]) + if i == 0 { + t := ctx.DockedWindows.Update(ctx.Windows[0].Bounds, ctx.Style.Scaling) + if t != nil { + ctx.DockedWindows = *t + } } } contextEnd(ctx) @@ -83,6 +83,26 @@ func (ctx *context) Update() { } } +func (ctx *context) updateWindow(win *Window) { + if win.updateFn != nil { + win.specialPanelBegin() + win.updateFn(win) + } + + if !win.began { + win.close = true + return + } + + if win.title == tooltipWindowTitle { + win.close = true + } + + if win.flags&windowPopup != 0 { + panelEnd(ctx, win) + } +} + func contextBegin(ctx *context, layout *panel) { for _, w := range ctx.Windows { w.usingSub = false @@ -91,6 +111,14 @@ func contextBegin(ctx *context, layout *panel) { w.widgets.reset() w.cmds.Reset() } + ctx.DockedWindows.Walk(func(w *Window) *Window { + w.usingSub = false + w.curNode = w.rootNode + w.close = false + w.widgets.reset() + w.cmds.Reset() + return w + }) ctx.trashFrame = false ctx.Windows[0].layout = layout @@ -128,6 +156,17 @@ func (ctx *context) Reset() { } func (ctx *context) Restack() { + clicked := false + for _, b := range []mouse.Button{mouse.ButtonLeft, mouse.ButtonRight, mouse.ButtonMiddle} { + if ctx.Input.Mouse.Buttons[b].Clicked && ctx.Input.Mouse.Buttons[b].Down { + clicked = true + break + } + } + if !clicked { + return + } + ctx.dockedWindowFocus = 0 nonmodalToplevel := false var toplevelIdx int for i := len(ctx.Windows) - 1; i >= 0; i-- { @@ -143,11 +182,13 @@ func (ctx *context) Restack() { // toplevel window is non-modal, proceed to change the stacking order if // the user clicked outside of it restacked := false + found := false for i := len(ctx.Windows) - 1; i > 0; i-- { if ctx.Windows[i].flags&windowTooltip != 0 { continue } - if ctx.restackClick(i) { + if ctx.restackClick(ctx.Windows[i]) { + found = true if toplevelIdx != i { newToplevel := ctx.Windows[i] copy(ctx.Windows[i:toplevelIdx], ctx.Windows[i+1:toplevelIdx+1]) @@ -162,21 +203,50 @@ func (ctx *context) Restack() { ctx.Windows[i].idx = i } } + if found { + return + } + ctx.DockedWindows.Walk(func(w *Window) *Window { + if ctx.restackClick(w) && (w.flags&windowDocked != 0) { + ctx.dockedWindowFocus = w.idx + } + return w + }) } -func (ctx *context) restackClick(i int) bool { +func (ctx *context) restackClick(w *Window) bool { if !ctx.Input.Mouse.valid { return false } for _, b := range []mouse.Button{mouse.ButtonLeft, mouse.ButtonRight, mouse.ButtonMiddle} { btn := ctx.Input.Mouse.Buttons[b] - if btn.Clicked && btn.Down && ctx.Windows[i].Bounds.Contains(btn.ClickedPos) { + if btn.Clicked && btn.Down && w.Bounds.Contains(btn.ClickedPos) { return true } } return false } +func (w *masterWindow) ListWindowsData() []interface{} { + return w.ctx.ListWindowsData() +} + +func (ctx *context) ListWindowsData() []interface{} { + r := []interface{}{} + ctx.DockedWindows.Walk(func(w *Window) *Window { + if w.Data != nil { + r = append(r, w.Data) + } + return w + }) + for _, w := range ctx.Windows { + if w.Data != nil { + r = append(r, w.Data) + } + } + return r +} + var cnt = 0 var ln, frect, brrect, frrect, ftri, circ, fcirc, txt int @@ -574,3 +644,228 @@ func (r *myRGBAPainter) Paint(ss []raster.Span, done bool) { } } } + +type dockedNodeType uint8 + +const ( + dockedNodeLeaf dockedNodeType = iota + dockedNodeVert + dockedNodeHoriz +) + +type dockedTree struct { + Type dockedNodeType + Split ScalableSplit + Child [2]*dockedTree + W *Window +} + +func (t *dockedTree) Update(bounds rect.Rect, scaling float64) *dockedTree { + if t == nil { + return nil + } + switch t.Type { + case dockedNodeVert: + b0, b1, _ := t.Split.verticalnw(bounds, scaling) + t.Child[0] = t.Child[0].Update(b0, scaling) + t.Child[1] = t.Child[1].Update(b1, scaling) + case dockedNodeHoriz: + b0, b1, _ := t.Split.horizontalnw(bounds, scaling) + t.Child[0] = t.Child[0].Update(b0, scaling) + t.Child[1] = t.Child[1].Update(b1, scaling) + case dockedNodeLeaf: + if t.W != nil { + t.W.Bounds = bounds + t.W.ctx.updateWindow(t.W) + if t.W == nil { + return nil + } + if t.W.close { + t.W = nil + return nil + } + return t + } + return nil + } + if t.Child[0] == nil { + return t.Child[1] + } + if t.Child[1] == nil { + return t.Child[0] + } + return t +} + +func (t *dockedTree) Walk(fn func(win *Window) *Window) { + if t == nil { + return + } + switch t.Type { + case dockedNodeVert, dockedNodeHoriz: + t.Child[0].Walk(fn) + t.Child[1].Walk(fn) + case dockedNodeLeaf: + if t.W != nil { + t.W = fn(t.W) + } + } +} + +func newDockedLeaf(win *Window) *dockedTree { + r := &dockedTree{Type: dockedNodeLeaf, W: win} + r.Split.MinSize = 40 + return r +} + +func (t *dockedTree) Dock(win *Window, pos image.Point, bounds rect.Rect, scaling float64) (bool, rect.Rect) { + if t == nil { + return false, rect.Rect{} + } + switch t.Type { + case dockedNodeVert: + b0, b1, _ := t.Split.verticalnw(bounds, scaling) + canDock, r := t.Child[0].Dock(win, pos, b0, scaling) + if canDock { + return canDock, r + } + canDock, r = t.Child[1].Dock(win, pos, b1, scaling) + if canDock { + return canDock, r + } + case dockedNodeHoriz: + b0, b1, _ := t.Split.horizontalnw(bounds, scaling) + canDock, r := t.Child[0].Dock(win, pos, b0, scaling) + if canDock { + return canDock, r + } + canDock, r = t.Child[1].Dock(win, pos, b1, scaling) + if canDock { + return canDock, r + } + case dockedNodeLeaf: + v := percentages(bounds, 0.03) + for i := range v { + if v[i].Contains(pos) { + if t.W == nil { + if win != nil { + t.W = win + win.ctx.dockWindow(win) + } + return true, bounds + } + w := percentages(bounds, 0.5) + if win != nil { + if i < 2 { + // horizontal split + t.Type = dockedNodeHoriz + t.Split.Size = int(float64(w[0].H) / scaling) + t.Child[i] = newDockedLeaf(win) + t.Child[-i+1] = newDockedLeaf(t.W) + } else { + // vertical split + t.Type = dockedNodeVert + t.Split.Size = int(float64(w[2].W) / scaling) + t.Child[i-2] = newDockedLeaf(win) + t.Child[-(i-2)+1] = newDockedLeaf(t.W) + } + + t.W = nil + win.ctx.dockWindow(win) + } + return true, w[i] + } + } + } + return false, rect.Rect{} +} + +func (ctx *context) dockWindow(win *Window) { + win.undockedSz = image.Point{win.Bounds.W, win.Bounds.H} + win.flags |= windowDocked + win.layout.Flags |= windowDocked + ctx.dockedCnt-- + win.idx = ctx.dockedCnt + for i := range ctx.Windows { + if ctx.Windows[i] == win { + if i+1 < len(ctx.Windows) { + copy(ctx.Windows[i:], ctx.Windows[i+1:]) + } + ctx.Windows = ctx.Windows[:len(ctx.Windows)-1] + return + } + } +} + +func (t *dockedTree) Undock(win *Window) { + t.Walk(func(w *Window) *Window { + if w == win { + return nil + } + return w + }) + win.flags &= ^windowDocked + win.layout.Flags &= ^windowDocked + win.Bounds.H = win.undockedSz.Y + win.Bounds.W = win.undockedSz.X + win.idx = len(win.ctx.Windows) + win.ctx.Windows = append(win.ctx.Windows, win) +} + +func (t *dockedTree) Scale(win *Window, delta image.Point, scaling float64) image.Point { + if t == nil || (delta.X == 0 && delta.Y == 0) { + return image.ZP + } + switch t.Type { + case dockedNodeVert: + d0 := t.Child[0].Scale(win, delta, scaling) + if d0.X != 0 { + t.Split.Size += int(float64(d0.X) / scaling) + if t.Split.Size <= t.Split.MinSize { + t.Split.Size = t.Split.MinSize + } + d0.X = 0 + } + if d0 != image.ZP { + return d0 + } + return t.Child[1].Scale(win, delta, scaling) + case dockedNodeHoriz: + d0 := t.Child[0].Scale(win, delta, scaling) + if d0.Y != 0 { + t.Split.Size += int(float64(d0.Y) / scaling) + if t.Split.Size <= t.Split.MinSize { + t.Split.Size = t.Split.MinSize + } + d0.Y = 0 + } + if d0 != image.ZP { + return d0 + } + return t.Child[1].Scale(win, delta, scaling) + case dockedNodeLeaf: + if t.W == win { + return delta + } + } + return image.ZP +} + +func percentages(bounds rect.Rect, f float64) (r [4]rect.Rect) { + pw := int(float64(bounds.W) * f) + ph := int(float64(bounds.H) * f) + // horizontal split + r[0] = bounds + r[0].H = ph + r[1] = bounds + r[1].Y += r[1].H - ph + r[1].H = ph + + // vertical split + r[2] = bounds + r[2].W = pw + r[3] = bounds + r[3].X += r[3].W - pw + r[3].W = pw + return +} diff --git a/input.go b/input.go index 2b9dc67..537a18c 100644 --- a/input.go +++ b/input.go @@ -113,6 +113,12 @@ func (win *Window) inputMaybe(widgetValid bool) *Input { } func (win *Window) toplevel() bool { + if win.moving { + return false + } + if win.ctx.dockedWindowFocus != 0 && win.idx == win.ctx.dockedWindowFocus { + return true + } for i := len(win.ctx.Windows) - 1; i >= 0; i-- { if win.ctx.Windows[i].flags&windowTooltip == 0 { return win.idx == i diff --git a/nucular.go b/nucular.go index 9ff3441..1d60c91 100644 --- a/nucular.go +++ b/nucular.go @@ -23,9 +23,11 @@ import ( /////////////////////////////////////////////////////////////////////////////////// type UpdateFn func(*Window) +type SaveFn func() []byte type Window struct { LastWidgetBounds rect.Rect + Data interface{} title string ctx *context idx int @@ -36,6 +38,8 @@ type Window struct { widgets widgetBuffer layout *panel close, first bool + moving bool + scaling bool // trigger rectangle of nonblocking windows header rect.Rect // root of the node tree @@ -50,12 +54,14 @@ type Window struct { editor *TextEditor // update function updateFn UpdateFn + saveFn SaveFn usingSub bool began bool rowCtor rowConstructor menuItemWidth int lastLayoutCnt int adjust map[int]map[int]*adjustCol + undockedSz image.Point } type FittingWidthFn func(width int) @@ -220,37 +226,12 @@ func panelBegin(ctx *context, win *Window, title string) { layout := win.layout wstyle := win.style() - /* cache style data */ window_padding := wstyle.Padding item_spacing := wstyle.Spacing scaler_size := wstyle.ScalerSize - /* check arguments */ *layout = panel{} - /* window dragging */ - if (win.flags&WindowMovable != 0) && win.toplevel() { - var move rect.Rect - move.X = win.Bounds.X - move.Y = win.Bounds.Y - move.W = win.Bounds.W - move.H = layout.HeaderH - - if win.idx != 0 { - move.H = FontHeight(font) + 2.0*wstyle.Header.Padding.Y - move.H += 2.0 * wstyle.Header.LabelPadding.Y - } else { - move.H = window_padding.Y + item_spacing.Y - } - - incursor := in.Mouse.PrevHoveringRect(move) - if in.Mouse.Down(mouse.ButtonLeft) && incursor { - delta := in.Mouse.Delta - win.Bounds.X = win.Bounds.X + delta.X - win.Bounds.Y = win.Bounds.Y + delta.Y - } - } - /* panel space with border */ if win.flags&WindowBorder != 0 { layout.Bounds = shrinkRect(win.Bounds, wstyle.Border) @@ -318,6 +299,8 @@ func panelBegin(ctx *context, win *Window, title string) { dwh.LayoutWidth = layout.Width dwh.Style = win.style() + var closeButton rect.Rect + if header_active { /* calculate header bounds */ dwh.Header.X = layout.Bounds.X @@ -338,14 +321,12 @@ func panelBegin(ctx *context, win *Window, title string) { dwh.Hovered = ctx.Input.Mouse.HoveringRect(dwh.Header) - header := dwh.Header - /* window header title */ t := FontWidth(font, title) - dwh.Label.X = header.X + wstyle.Header.Padding.X + dwh.Label.X = dwh.Header.X + wstyle.Header.Padding.X dwh.Label.X += wstyle.Header.LabelPadding.X - dwh.Label.Y = header.Y + wstyle.Header.LabelPadding.Y + dwh.Label.Y = dwh.Header.Y + wstyle.Header.LabelPadding.Y dwh.Label.H = FontHeight(font) + 2*wstyle.Header.LabelPadding.Y dwh.Label.W = t + 2*wstyle.Header.Spacing.X dwh.LayoutHeaderH = layout.HeaderH @@ -355,21 +336,20 @@ func panelBegin(ctx *context, win *Window, title string) { win.widgets.Add(nstyle.WidgetStateInactive, layout.Bounds) dwh.Draw(&win.ctx.Style, &win.cmds) - var button rect.Rect - /* window close button */ - button.Y = header.Y + wstyle.Header.Padding.Y - button.H = layout.HeaderH - 2*wstyle.Header.Padding.Y - button.W = button.H + // window close button + closeButton.Y = dwh.Header.Y + wstyle.Header.Padding.Y + closeButton.H = layout.HeaderH - 2*wstyle.Header.Padding.Y + closeButton.W = closeButton.H if win.flags&WindowClosable != 0 { if wstyle.Header.Align == nstyle.HeaderRight { - button.X = (header.W + header.X) - (button.W + wstyle.Header.Padding.X) - header.W -= button.W + wstyle.Header.Spacing.X + wstyle.Header.Padding.X + closeButton.X = (dwh.Header.W + dwh.Header.X) - (closeButton.W + wstyle.Header.Padding.X) + dwh.Header.W -= closeButton.W + wstyle.Header.Spacing.X + wstyle.Header.Padding.X } else { - button.X = header.X + wstyle.Header.Padding.X - header.X += button.W + wstyle.Header.Spacing.X + wstyle.Header.Padding.X + closeButton.X = dwh.Header.X + wstyle.Header.Padding.X + dwh.Header.X += closeButton.W + wstyle.Header.Spacing.X + wstyle.Header.Padding.X } - if doButton(win, label.S(wstyle.Header.CloseSymbol), button, &wstyle.Header.CloseButton, in, false) { + if doButton(win, label.S(wstyle.Header.CloseSymbol), closeButton, &wstyle.Header.CloseButton, in, false) { win.close = true } } @@ -380,6 +360,28 @@ func panelBegin(ctx *context, win *Window, title string) { dwh.Draw(&win.ctx.Style, &win.cmds) } + // window dragging + if win.moving { + if in == nil || !in.Mouse.Down(mouse.ButtonLeft) { + if win.flags&windowDocked == 0 && in != nil { + win.ctx.DockedWindows.Dock(win, in.Mouse.Pos, win.ctx.Windows[0].Bounds, win.ctx.Style.Scaling) + } + win.moving = false + } else { + win.move(in.Mouse.Delta, in.Mouse.Pos) + } + } else if (win.flags&WindowMovable != 0) && win.toplevel() { + var move rect.Rect + move.X = win.Bounds.X + move.Y = win.Bounds.Y + move.W = win.Bounds.W + move.H = FontHeight(font) + 2.0*wstyle.Header.Padding.Y + 2.0*wstyle.Header.LabelPadding.Y + + if in.Mouse.IsClickDownInRect(mouse.ButtonLeft, move, true) && !in.Mouse.IsClickDownInRect(mouse.ButtonLeft, closeButton, true) { + win.moving = true + } + } + var dwb drawableWindowBody dwb.NoScrollbar = win.flags&WindowNoScrollbar != 0 @@ -650,26 +652,16 @@ func panelEnd(ctx *context, window *Window) { dsab.ScalerRect.Y = layout.Bounds.Y + layout.Bounds.H - (scaler_size.Y + window_padding.Y) } - scalingRect := dsab.ScalerRect - if layout.Flags&windowDocked != 0 { - scalingRect = layout.Bounds - scalingRect.Y = dsab.ScalerRect.Y - } - /* do window scaling logic */ if window.toplevel() { - prev := in.Mouse.Prev - window_size := wstyle.MinSize - - incursor := scalingRect.Contains(prev) - - if in != nil && in.Mouse.Down(mouse.ButtonLeft) && incursor { - window.Bounds.W = max(window_size.X, window.Bounds.W+in.Mouse.Delta.X) - - /* dragging in y-direction is only possible if static window */ - if layout.Flags&WindowDynamic == 0 { - window.Bounds.H = max(window_size.Y, window.Bounds.H+in.Mouse.Delta.Y) + if window.scaling { + if in == nil || !in.Mouse.Down(mouse.ButtonLeft) { + window.scaling = false + } else { + window.scale(in.Mouse.Delta) } + } else if in != nil && in.Mouse.IsClickDownInRect(mouse.ButtonLeft, dsab.ScalerRect, true) { + window.scaling = true } } } @@ -716,6 +708,34 @@ func (win *Window) MenubarBegin() { layout.Offset.Y = 0 } +func (win *Window) move(delta image.Point, pos image.Point) { + if win.flags&windowDocked != 0 { + if delta.X != 0 && delta.Y != 0 { + win.ctx.DockedWindows.Undock(win) + } + return + } + if canDock, bounds := win.ctx.DockedWindows.Dock(nil, pos, win.ctx.Windows[0].Bounds, win.ctx.Style.Scaling); canDock { + win.cmds.FillRect(bounds, 0, color.RGBA{0x0, 0x0, 0x50, 0x50}) + } + win.Bounds.X = win.Bounds.X + delta.X + win.Bounds.Y = win.Bounds.Y + delta.Y +} + +func (win *Window) scale(delta image.Point) { + if win.flags&windowDocked != 0 { + win.ctx.DockedWindows.Scale(win, delta, win.ctx.Style.Scaling) + return + } + window_size := win.style().MinSize + win.Bounds.W = max(window_size.X, win.Bounds.W+delta.X) + + /* dragging in y-direction is only possible if static window */ + if win.layout.Flags&WindowDynamic == 0 { + win.Bounds.H = max(window_size.Y, win.Bounds.H+delta.Y) + } +} + // MenubarEnd signals that all widgets have been added to the menubar. func (win *Window) MenubarEnd() { layout := win.layout @@ -805,9 +825,6 @@ func panelLayout(ctx *context, win *Window, height int, cols int, cnt int) { if height == 0 { height = layout.Clip.H - (layout.AtY - layout.Bounds.Y) subtractHeight := true - if layout.Row.Columns > 0 && layout.Row.Index >= layout.Row.Columns { - subtractHeight = false - } if layout.Row.Index == 0 { subtractHeight = false } @@ -1798,7 +1815,7 @@ const ( horizontal ) -func scrollbarBehavior(state *nstyle.WidgetStates, in *Input, scroll, cursor, scrollwheel_bounds, empty0, empty1 rect.Rect, scroll_offset float64, target float64, scroll_step float64, o orientation) float64 { +func scrollbarBehavior(state *nstyle.WidgetStates, in *Input, scroll, cursor, empty0, empty1 rect.Rect, scroll_offset float64, target float64, scroll_step float64, o orientation) float64 { exitstate := basicWidgetStateControl(state, in, cursor) if *state == nstyle.WidgetStateActive { @@ -1843,16 +1860,17 @@ func scrollbarBehavior(state *nstyle.WidgetStates, in *Input, scroll, cursor, sc } } - if o == vertical && ((in.Mouse.ScrollDelta < 0) || (in.Mouse.ScrollDelta > 0)) && in.Mouse.HoveringRect(scrollwheel_bounds) { + return scroll_offset +} + +func scrollwheelBehavior(win *Window, scroll, scrollwheel_bounds rect.Rect, scroll_offset, target, scroll_step float64) float64 { + in := &win.ctx.Input + + if ((in.Mouse.ScrollDelta < 0) || (in.Mouse.ScrollDelta > 0)) && in.Mouse.HoveringRect(scrollwheel_bounds) { /* update cursor by mouse scrolling */ old_scroll_offset := scroll_offset scroll_offset = scroll_offset + scroll_step*float64(-in.Mouse.ScrollDelta) - - if o == vertical { - scroll_offset = clampFloat(0, scroll_offset, target-float64(scroll.H)) - } else { - scroll_offset = clampFloat(0, scroll_offset, target-float64(scroll.W)) - } + scroll_offset = clampFloat(0, scroll_offset, target-float64(scroll.H)) used_delta := (scroll_offset - old_scroll_offset) / scroll_step residual := float64(in.Mouse.ScrollDelta) + used_delta if residual < 0 { @@ -1861,7 +1879,6 @@ func scrollbarBehavior(state *nstyle.WidgetStates, in *Input, scroll, cursor, sc in.Mouse.ScrollDelta = int(math.Floor(residual)) } } - return scroll_offset } @@ -1935,7 +1952,8 @@ func doScrollbarv(win *Window, scroll, scrollwheel_bounds rect.Rect, offset floa /* update scrollbar */ out := &win.widgets state := out.PrevState(scroll) - scroll_offset = scrollbarBehavior(&state, in, scroll, cursor, scrollwheel_bounds, emptyNorth, emptySouth, scroll_offset, target, scroll_step, vertical) + scroll_offset = scrollbarBehavior(&state, in, scroll, cursor, emptyNorth, emptySouth, scroll_offset, target, scroll_step, vertical) + scroll_offset = scrollwheelBehavior(win, scroll, scrollwheel_bounds, scroll_offset, target, scroll_step) scroll_off = scroll_offset / target cursor.Y = scroll.Y + int(scroll_off*float64(scroll.H)) @@ -2018,7 +2036,7 @@ func doScrollbarh(win *Window, scroll rect.Rect, offset float64, target float64, /* update scrollbar */ out := &win.widgets state := out.PrevState(scroll) - scroll_offset = scrollbarBehavior(&state, in, scroll, cursor, rect.Rect{0, 0, 0, 0}, emptyWest, emptyEast, scroll_offset, target, scroll_step, horizontal) + scroll_offset = scrollbarBehavior(&state, in, scroll, cursor, emptyWest, emptyEast, scroll_offset, target, scroll_step, horizontal) scroll_off = scroll_offset / target cursor.X = scroll.X + int(scroll_off*float64(scroll.W)) @@ -2606,15 +2624,28 @@ func (mw *masterWindow) PopupOpen(title string, flags WindowFlags, rect rect.Rec go func() { mw.uilock.Lock() defer mw.uilock.Unlock() - mw.ctx.popupOpen(title, flags, rect, scale, updateFn) + mw.ctx.popupOpen(title, flags, rect, scale, updateFn, nil) + mw.Changed() + }() +} + +func (mw *masterWindow) PopupOpenPersistent(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn, saveFn SaveFn) { + if flags&WindowNonmodal == 0 && saveFn != nil { + panic("save function set on modal window") + } + go func() { + mw.uilock.Lock() + defer mw.uilock.Unlock() + mw.ctx.popupOpen(title, flags, rect, scale, updateFn, saveFn) mw.Changed() }() } -func (ctx *context) popupOpen(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn) { +func (ctx *context) popupOpen(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn, saveFn SaveFn) { popup := createWindow(ctx, title) popup.idx = len(ctx.Windows) popup.updateFn = updateFn + popup.saveFn = saveFn if updateFn == nil { panic("nil update function") } @@ -2743,7 +2774,7 @@ func (win *Window) TooltipOpen(width int, scale bool, updateFn UpdateFn) { bounds.X = (in.Mouse.Pos.X + 1) bounds.Y = (in.Mouse.Pos.Y + 1) - win.ctx.popupOpen(tooltipWindowTitle, WindowDynamic|WindowNoScrollbar|windowTooltip, bounds, false, updateFn) + win.ctx.popupOpen(tooltipWindowTitle, WindowDynamic|WindowNoScrollbar|windowTooltip, bounds, false, updateFn, nil) } // Shows a tooltip window containing the specified text. diff --git a/restore.go b/restore.go new file mode 100644 index 0000000..b9840e0 --- /dev/null +++ b/restore.go @@ -0,0 +1,223 @@ +package nucular + +import ( + "bufio" + "bytes" + "encoding/base64" + "fmt" + "image" + "io" + "os" + "strconv" + + "github.com/aarzilli/nucular/rect" +) + +type OpenWindowFn func(title string, flags WindowFlags, updateFn UpdateFn, saveFn SaveFn) +type RestoreFn func(data []byte, openWndFn OpenWindowFn) error + +func (w *masterWindow) Save() ([]byte, error) { + var out bytes.Buffer + err := w.ctx.Save(&out) + return out.Bytes(), err +} + +func (w *masterWindow) Restore(data []byte, restoreFn RestoreFn) { + go func() { + w.uilock.Lock() + defer w.uilock.Unlock() + err := w.ctx.Restore(bytes.NewBuffer(data), restoreFn) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + } + w.Changed() + }() +} + +/* +Save format ::= +floating_configs ::= ,,,,[]* + ::= 0 | "|" | "_" | +window ::= a single character other than '0', '|', '_' or '!'. Or '!' followed by a base64 encoding terminated by another '!' +*/ + +func (ctx *context) Save(w io.Writer) error { + err := ctx.DockedWindows.Save(w) + if err != nil { + return err + } + for _, wnd := range ctx.Windows { + if wnd.flags&WindowNonmodal == 0 { + return fmt.Errorf("can not save when non-modal windows are open") + } + fmt.Fprintf(w, "%d,%d,%d,%d,", wnd.Bounds.X, wnd.Bounds.Y, wnd.Bounds.W, wnd.Bounds.H) + saveWindow(w, wnd) + } + return nil +} + +func (t *dockedTree) Save(w io.Writer) error { + switch t.Type { + case dockedNodeVert: + fmt.Fprintf(w, "|%d", t.Split.Size) + err := t.Child[0].Save(w) + if err != nil { + return err + } + err = t.Child[1].Save(w) + if err != nil { + return err + } + case dockedNodeHoriz: + fmt.Fprintf(w, "_%d", t.Split.Size) + err := t.Child[0].Save(w) + if err != nil { + return err + } + err = t.Child[1].Save(w) + if err != nil { + return err + } + case dockedNodeLeaf: + if t.W == nil { + fmt.Fprintf(w, "0") + } else { + if t.W.saveFn == nil { + return fmt.Errorf("one docked window doesn't have a save function") + } + saveWindow(w, t.W) + } + } + return nil +} + +func saveWindow(w io.Writer, wnd *Window) { + if wnd.saveFn != nil { + data := wnd.saveFn() + switch { + case len(data) == 0: + fmt.Fprintf(w, "0") + case len(data) == 1 && data[0] != '0' && data[0] != '|' && data[0] != '_' && data[0] != '!' && data[0] >= '0' && data[0] <= 'z': + fmt.Fprintf(w, "%c", data[0]) + default: + encodedata := base64.StdEncoding.EncodeToString(data) + fmt.Fprintf(w, "!%s!", encodedata) + } + } else { + fmt.Fprintf(w, "0") + } +} + +func (ctx *context) Restore(in io.Reader, restoreFn RestoreFn) error { + rd := bufio.NewReader(in) + ctx.DockedWindows = dockedTree{} + layout0 := ctx.Windows[0].layout + ctx.Windows = ctx.Windows[:0] + ctx.dockedCnt = 0 + ctx.DockedWindows = *parseDockedTree(rd, ctx, restoreFn) + + for { + var rect rect.Rect + rect.X = readIntComma(rd) + rect.Y = readIntComma(rd) + rect.W = readIntComma(rd) + rect.H = readIntComma(rd) + if !readWindow(layout0, rd, ctx, rect, restoreFn) { + break + } + layout0 = nil + } + + for i, w := range ctx.Windows { + w.idx = i + } + + return nil +} + +func parseDockedTree(rd *bufio.Reader, ctx *context, restoreFn RestoreFn) *dockedTree { + switch b, _ := rd.ReadByte(); b { + case '0': + return &dockedTree{} + case '_', '|': + t := &dockedTree{} + t.Split.Size = readIntNoComma(rd) + t.Type = dockedNodeHoriz + if b == '|' { + t.Type = dockedNodeVert + } + t.Child[0] = parseDockedTree(rd, ctx, restoreFn) + t.Child[1] = parseDockedTree(rd, ctx, restoreFn) + return t + default: + t := &dockedTree{} + t.Type = dockedNodeLeaf + finishReadWindow(b, nil, rd, ctx, rect.Rect{0, 0, 200, 200}, restoreFn) + t.W = ctx.Windows[len(ctx.Windows)-1] + ctx.Windows = ctx.Windows[:len(ctx.Windows)-1] + t.W.flags |= windowDocked + ctx.dockedCnt-- + t.W.idx = ctx.dockedCnt + t.W.undockedSz = image.Point{t.W.Bounds.W, t.W.Bounds.H} + return t + } +} + +func readIntComma(rd *bufio.Reader) int { + bs, _ := rd.ReadBytes(',') + if len(bs) == 0 { + return 0 + } + n, _ := strconv.Atoi(string(bs[:len(bs)-1])) + return n +} + +func readIntNoComma(rd *bufio.Reader) int { + r := 0 + for { + b, err := rd.ReadByte() + if err != nil { + break + } + if b < '0' || b > '9' { + rd.UnreadByte() + break + } + r = r * 10 + r += int(b - '0') + } + return r +} + +func readWindow(layout0 *panel, rd *bufio.Reader, ctx *context, rect rect.Rect, restoreFn RestoreFn) bool { + b, err := rd.ReadByte() + if err != nil { + return false + } + finishReadWindow(b, layout0, rd, ctx, rect, restoreFn) + return true +} + +func finishReadWindow(b byte, layout0 *panel, rd *bufio.Reader, ctx *context, rect rect.Rect, restoreFn RestoreFn) { + if b == '0' { + if layout0 != nil { + ctx.setupMasterWindow(layout0, func(*Window) {}) + } else { + ctx.popupOpen("", WindowDefaultFlags, rect, false, func(*Window) {}, nil) + } + return + } + data := []byte{b} + if b == '!' { + data, _ = rd.ReadBytes('!') + data, _ = base64.StdEncoding.DecodeString(string(data)) + } + called := false + restoreFn(data, func(title string, flags WindowFlags, updateFn UpdateFn, saveFn SaveFn) { + if called { + return + } + called = true + ctx.popupOpen(title, flags, rect, false, updateFn, saveFn) + }) +} diff --git a/shiny.go b/shiny.go index 2c61b37..b71dcf2 100644 --- a/shiny.go +++ b/shiny.go @@ -53,6 +53,11 @@ type MasterWindow interface { SetPerf(bool) PopupOpen(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn) + PopupOpenPersistent(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn, saveFn SaveFn) + + Save() ([]byte, error) + Restore([]byte, RestoreFn) + ListWindowsData() []interface{} } type masterWindow struct { @@ -87,6 +92,7 @@ func NewMasterWindow(flags WindowFlags, title string, updatefn UpdateFn) MasterW func NewMasterWindowSize(flags WindowFlags, title string, sz image.Point, updatefn UpdateFn) MasterWindow { ctx := &context{} ctx.Input.Mouse.valid = true + ctx.DockedWindows.Split.MinSize = 40 wnd := &masterWindow{ctx: ctx} wnd.layout.Flags = flags wnd.Title = title diff --git a/split.go b/split.go index 6aa3c32..d02cc5f 100644 --- a/split.go +++ b/split.go @@ -16,10 +16,35 @@ type ScalableSplit struct { } func (s *ScalableSplit) Horizontal(w *Window, bounds rect.Rect) (bounds0, bounds1 rect.Rect) { + scaling := w.Master().Style().Scaling + + var rszbounds rect.Rect + bounds0, bounds1, rszbounds = s.horizontalnw(bounds, scaling) + + w.LayoutSpacePushScaled(rszbounds) + rszbounds, _ = w.Custom(nstyle.WidgetStateInactive) + + if w.Input().Mouse.IsClickDownInRect(mouse.ButtonLeft, rszbounds, true) { + s.resize = true + } + if s.resize { + if !w.Input().Mouse.Down(mouse.ButtonLeft) { + s.resize = false + } else { + s.Size += int(float64(w.Input().Mouse.Delta.Y) / scaling) + if s.Size <= s.MinSize { + s.Size = s.MinSize + } + } + } + + return +} + +func (s *ScalableSplit) horizontalnw(bounds rect.Rect, scaling float64) (bounds0, bounds1, rszbounds rect.Rect) { if bounds.H < 0 || bounds.W < 0 { return } - scaling := w.Master().Style().Scaling if s.lastsize == 0 { s.lastsize = bounds.H @@ -55,7 +80,7 @@ func (s *ScalableSplit) Horizontal(w *Window, bounds rect.Rect) (bounds0, bounds bounds0 = bounds bounds0.H = h0 - rszbounds := bounds + rszbounds = bounds rszbounds.Y += bounds0.H rszbounds.H = hs @@ -63,6 +88,15 @@ func (s *ScalableSplit) Horizontal(w *Window, bounds rect.Rect) (bounds0, bounds bounds1.Y = rszbounds.Y + rszbounds.H bounds1.H = h1 + return bounds0, bounds1, rszbounds +} + +func (s *ScalableSplit) Vertical(w *Window, bounds rect.Rect) (bounds0, bounds1 rect.Rect) { + scaling := w.Master().Style().Scaling + + var rszbounds rect.Rect + bounds0, bounds1, rszbounds = s.verticalnw(bounds, scaling) + w.LayoutSpacePushScaled(rszbounds) rszbounds, _ = w.Custom(nstyle.WidgetStateInactive) @@ -73,7 +107,7 @@ func (s *ScalableSplit) Horizontal(w *Window, bounds rect.Rect) (bounds0, bounds if !w.Input().Mouse.Down(mouse.ButtonLeft) { s.resize = false } else { - s.Size += int(float64(w.Input().Mouse.Delta.Y) / scaling) + s.Size += int(float64(w.Input().Mouse.Delta.X) / scaling) if s.Size <= s.MinSize { s.Size = s.MinSize } @@ -83,11 +117,10 @@ func (s *ScalableSplit) Horizontal(w *Window, bounds rect.Rect) (bounds0, bounds return bounds0, bounds1 } -func (s *ScalableSplit) Vertical(w *Window, bounds rect.Rect) (bounds0, bounds1 rect.Rect) { +func (s *ScalableSplit) verticalnw(bounds rect.Rect, scaling float64) (bounds0, bounds1, rszbounds rect.Rect) { if bounds.H < 0 || bounds.W < 0 { return } - scaling := w.Master().Style().Scaling if s.lastsize == 0 { s.lastsize = bounds.W @@ -123,7 +156,7 @@ func (s *ScalableSplit) Vertical(w *Window, bounds rect.Rect) (bounds0, bounds1 bounds0 = bounds bounds0.W = w0 - rszbounds := bounds + rszbounds = bounds rszbounds.X += bounds0.W rszbounds.W = ws @@ -131,22 +164,5 @@ func (s *ScalableSplit) Vertical(w *Window, bounds rect.Rect) (bounds0, bounds1 bounds1.X = rszbounds.X + rszbounds.W bounds1.W = w1 - w.LayoutSpacePushScaled(rszbounds) - rszbounds, _ = w.Custom(nstyle.WidgetStateInactive) - - if w.Input().Mouse.IsClickDownInRect(mouse.ButtonLeft, rszbounds, true) { - s.resize = true - } - if s.resize { - if !w.Input().Mouse.Down(mouse.ButtonLeft) { - s.resize = false - } else { - s.Size += int(float64(w.Input().Mouse.Delta.X) / scaling) - if s.Size <= s.MinSize { - s.Size = s.MinSize - } - } - } - - return bounds0, bounds1 + return bounds0, bounds1, rszbounds } diff --git a/testing.go b/testing.go index ac1e368..e8b054c 100644 --- a/testing.go +++ b/testing.go @@ -79,7 +79,11 @@ func (w *TestWindow) SetPerf(p bool) { } func (w *TestWindow) PopupOpen(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn) { - w.ctx.popupOpen(title, flags, rect, scale, updateFn) + w.ctx.popupOpen(title, flags, rect, scale, updateFn, nil) +} + +func (w *TestWindow) PopupOpenPersistent(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn, saveFn SaveFn) { + w.ctx.popupOpen(title, flags, rect, scale, updateFn, saveFn) } // Click simulates a click at point p. @@ -123,3 +127,15 @@ func (w *TestWindow) TypeKey(e key.Event) { w.ctx.Input.Keyboard.Text = w.ctx.Input.Keyboard.Text + b.String() w.Update() } + +func (w *TestWindow) Save() ([]byte, error) { + return nil, nil +} + +func (w *TestWindow) Restore([]byte, RestoreFn) { + return +} + +func (w *TestWindow) ListWindowsData() []interface{} { + return w.ctx.ListWindowsData() +}