Procházet zdrojové kódy

improved gui/window

Daniel Salvadori před 7 roky
rodič
revize
bf13fb0040
5 změnil soubory, kde provedl 207 přidání a 162 odebrání
  1. 2 21
      gui/builder.go
  2. 1 1
      gui/builder_panel.go
  3. 9 9
      gui/style_dark.go
  4. 9 9
      gui/style_light.go
  5. 186 122
      gui/window.go

+ 2 - 21
gui/builder.go

@@ -177,15 +177,6 @@ var mapEdgeName = map[string]int{
 	"center": DockCenter,
 }
 
-// maps resize border name (window) with parameter value
-var mapResizable = map[string]ResizeBorders{
-	"top":    ResizeTop,
-	"right":  ResizeRight,
-	"bottom": ResizeBottom,
-	"left":   ResizeLeft,
-	"all":    ResizeAll,
-}
-
 // maps table sort type to value
 var mapTableSortType = map[string]TableSortType{
 	"none":   TableSortNone,
@@ -698,22 +689,12 @@ func AttribCheckResizeBorders(b *Builder, am map[string]interface{}, fname strin
 	}
 
 	// Attribute must be string
-	vs, ok := v.(string)
+	vs, ok := v.(bool)
 	if !ok {
 		return b.err(am, fname, "Invalid resizable attribute")
 	}
 
-	// Each string field must be a valid resizable name
-	parts := strings.Fields(vs)
-	var res ResizeBorders
-	for _, name := range parts {
-		v, ok := mapResizable[name]
-		if !ok {
-			return b.err(am, fname, "Invalid resizable name:"+name)
-		}
-		res |= v
-	}
-	am[fname] = res
+	am[fname] = vs
 	return nil
 }
 

+ 1 - 1
gui/builder_panel.go

@@ -609,7 +609,7 @@ func buildWindow(b *Builder, am map[string]interface{}) (IPanel, error) {
 
 	// Set optional resizable borders
 	if resiz := am[AttribResizeBorders]; resiz != nil {
-		win.SetResizable(resiz.(ResizeBorders))
+		win.SetResizable(resiz.(bool))
 	}
 
 	// Builds window children

+ 9 - 9
gui/style_dark.go

@@ -154,15 +154,15 @@ func NewDarkStyle() *Style {
 
 	// Window styles
 	s.Window = WindowStyles{}
-	s.Window.Normal = WindowStyle{
-		Border:           RectBounds{2, 2, 2, 2},
-		Paddings:         zeroBounds,
-		BorderColor:      s.Color.BgDark,
-		TitleBorders:     RectBounds{0, 0, 1, 0},
-		TitleBorderColor: math32.Color4{0, 0, 0, 1},
-		TitleBgColor:     s.Color.Select,
-		TitleFgColor:     s.Color.Text,
-	}
+	s.Window.Normal = WindowStyle{}
+	s.Window.Normal.Border = RectBounds{2, 2, 2, 2}
+	s.Window.Normal.Padding = zeroBounds
+	s.Window.Normal.BorderColor = s.Color.BgDark
+	s.Window.Normal.TitleStyle = WindowTitleStyle{}
+	s.Window.Normal.TitleStyle.Border = RectBounds{0, 0, 1, 0}
+	s.Window.Normal.TitleStyle.BorderColor = math32.Color4{0, 0, 0, 1}
+	s.Window.Normal.TitleStyle.BgColor = s.Color.Select
+	s.Window.Normal.TitleStyle.FgColor = s.Color.Text
 	s.Window.Over = s.Window.Normal
 	s.Window.Focus = s.Window.Normal
 	s.Window.Disabled = s.Window.Normal

+ 9 - 9
gui/style_light.go

@@ -151,15 +151,15 @@ func NewLightStyle() *Style {
 
 	// Window styles
 	s.Window = WindowStyles{}
-	s.Window.Normal = WindowStyle{
-		Border:           RectBounds{4, 4, 4, 4},
-		Paddings:         zeroBounds,
-		BorderColor:      math32.Color4{0.2, 0.2, 0.2, 1},
-		TitleBorders:     RectBounds{0, 0, 1, 0},
-		TitleBorderColor: math32.Color4{0, 0, 0, 1},
-		TitleBgColor:     math32.Color4{0, 1, 0, 1},
-		TitleFgColor:     math32.Color4{0, 0, 0, 1},
-	}
+	s.Window.Normal = WindowStyle{}
+	s.Window.Normal.Border = RectBounds{4, 4, 4, 4}
+	s.Window.Normal.Padding = zeroBounds
+	s.Window.Normal.BorderColor = math32.Color4{0.2, 0.2, 0.2, 1}
+	s.Window.Normal.TitleStyle = WindowTitleStyle{}
+	s.Window.Normal.TitleStyle.Border = RectBounds{0, 0, 1, 0}
+	s.Window.Normal.TitleStyle.BorderColor = math32.Color4{0, 0, 0, 1}
+	s.Window.Normal.TitleStyle.BgColor = math32.Color4{0, 1, 0, 1} // s.Color.Select
+	s.Window.Normal.TitleStyle.FgColor = math32.Color4{0, 0, 0, 1} // s.Color.Text
 	s.Window.Over = s.Window.Normal
 	s.Window.Focus = s.Window.Normal
 	s.Window.Disabled = s.Window.Normal

+ 186 - 122
gui/window.go

@@ -7,14 +7,15 @@ package gui
 import (
 	"github.com/g3n/engine/math32"
 	"github.com/g3n/engine/window"
+	"github.com/g3n/engine/gui/assets/icon"
 )
 
 /*********************************************
 
  Window panel
- +-----------------------------------------+
- | Title panel                             |
- +-----------------------------------------+
+ +-------------------------------------+---+
+ |              Title panel            | X |
+ +-------------------------------------+---+
  |  Content panel                          |
  |  +-----------------------------------+  |
  |  |                                   |  |
@@ -31,26 +32,31 @@ import (
 
 // Window represents a window GUI element
 type Window struct {
-	Panel      // Embedded Panel
-	styles     *WindowStyles
-	title      *WindowTitle // internal optional title panel
-	client     Panel        // internal client panel
-	resizable  ResizeBorders
-	overBorder string
-	drag       bool
-	mouseX     float32
-	mouseY     float32
+	Panel       // Embedded Panel
+	styles      *WindowStyles
+	title       *WindowTitle // internal optional title panel
+	client      Panel        // internal client panel
+	resizable   bool         // Specifies whether the window is resizable
+	drag        bool    // Whether the mouse buttons is pressed (i.e. when dragging)
+	dragPadding float32 // Extra width used to resize (in addition to border sizes)
+
+	// To keep track of which window borders the cursor is over
+	overTop    bool
+	overRight  bool
+	overBottom bool
+	overLeft   bool
+
+	// Minimum and maximum sizes
+	minSize math32.Vector2
+	maxSize math32.Vector2
 }
 
+// TODO: Fix window builder examples
+
 // WindowStyle contains the styling of a Window
 type WindowStyle struct {
-	Border           RectBounds
-	Paddings         RectBounds
-	BorderColor      math32.Color4
-	TitleBorders     RectBounds
-	TitleBorderColor math32.Color4
-	TitleBgColor     math32.Color4
-	TitleFgColor     math32.Color4
+	PanelStyle
+	TitleStyle WindowTitleStyle
 }
 
 // WindowStyles contains a WindowStyle for each valid GUI state
@@ -61,18 +67,6 @@ type WindowStyles struct {
 	Disabled WindowStyle
 }
 
-// ResizeBorders specifies which window borders can be resized
-type ResizeBorders int
-
-// Resizing can be allowed or disallowed on each window edge
-const (
-	ResizeTop = ResizeBorders(1 << (iota + 1))
-	ResizeRight
-	ResizeBottom
-	ResizeLeft
-	ResizeAll = ResizeTop | ResizeRight | ResizeBottom | ResizeLeft
-)
-
 // NewWindow creates and returns a pointer to a new window with the
 // specified dimensions
 func NewWindow(width, height float32) *Window {
@@ -91,18 +85,26 @@ func NewWindow(width, height float32) *Window {
 	w.client.Initialize(0, 0)
 	w.Panel.Add(&w.client)
 
+	w.dragPadding = 5
+
 	w.recalc()
 	w.update()
 	return w
 }
 
-// SetResizable set the borders which are resizable
-func (w *Window) SetResizable(res ResizeBorders) {
+// SetResizable sets whether the window is resizable.
+func (w *Window) SetResizable(state bool) {
 
-	w.resizable = res
+	w.resizable = state
 }
 
-// SetTitle sets the title of this window
+// SetCloseButton sets whether the window has a close button on the top right.
+func (w *Window) SetCloseButton(state bool) {
+
+	w.title.setCloseButton(state)
+}
+
+// SetTitle sets the title of the window.
 func (w *Window) SetTitle(text string) {
 
 	if w.title == nil {
@@ -122,7 +124,7 @@ func (w *Window) Add(ichild IPanel) *Window {
 	return w
 }
 
-// SetLayout set the layout of this window content area
+// SetLayout sets the layout of the client panel.
 func (w *Window) SetLayout(layout ILayout) {
 
 	w.client.SetLayout(layout)
@@ -131,20 +133,18 @@ func (w *Window) SetLayout(layout ILayout) {
 // onMouse process subscribed mouse events over the window
 func (w *Window) onMouse(evname string, ev interface{}) {
 
-	mev := ev.(*window.MouseEvent)
 	switch evname {
 	case OnMouseDown:
-		par := w.Parent().(IPanel).GetPanel()
-		par.SetTopChild(w)
-		if w.overBorder != "" {
+		// Move the window above everything contained in its parent
+		parent := w.Parent().(IPanel).GetPanel()
+		parent.SetTopChild(w)
+		// If the click happened inside the draggable area, then set drag to true
+		if w.overTop || w.overRight || w.overBottom || w.overLeft {
 			w.drag = true
-			w.mouseX = mev.Xpos
-			w.mouseY = mev.Ypos
 			w.root.SetMouseFocus(w)
 		}
 	case OnMouseUp:
 		w.drag = false
-		w.root.SetCursorNormal()
 		w.root.SetMouseFocus(nil)
 	default:
 		return
@@ -155,78 +155,103 @@ func (w *Window) onMouse(evname string, ev interface{}) {
 // onCursor process subscribed cursor events over the window
 func (w *Window) onCursor(evname string, ev interface{}) {
 
+	// If the window is not resizable we are not interested in cursor movements
+	if !w.resizable {
+		return
+	}
 	if evname == OnCursor {
 		cev := ev.(*window.CursorEvent)
-		if !w.drag {
-			cx := cev.Xpos - w.pospix.X
-			cy := cev.Ypos - w.pospix.Y
-			if cy <= w.borderSizes.Top {
-				if w.resizable&ResizeTop != 0 {
-					w.overBorder = "top"
-					w.root.SetCursorVResize()
-				}
-			} else if cy >= w.height-w.borderSizes.Bottom {
-				if w.resizable&ResizeBottom != 0 {
-					w.overBorder = "bottom"
-					w.root.SetCursorVResize()
-				}
-			} else if cx <= w.borderSizes.Left {
-				if w.resizable&ResizeLeft != 0 {
-					w.overBorder = "left"
-					w.root.SetCursorHResize()
-				}
-			} else if cx >= w.width-w.borderSizes.Right {
-				if w.resizable&ResizeRight != 0 {
-					w.overBorder = "right"
-					w.root.SetCursorHResize()
-				}
-			} else {
-				if w.overBorder != "" {
-					w.root.SetCursorNormal()
-					w.overBorder = ""
-				}
-			}
-		} else {
-			switch w.overBorder {
-			case "top":
-				delta := cev.Ypos - w.mouseY
-				w.mouseY = cev.Ypos
+		// If already dragging - update window size and position depending
+		// on the cursor position and the borders being dragged
+		if w.drag {
+			if w.overTop {
+				delta := cev.Ypos - w.pospix.Y
 				newHeight := w.Height() - delta
-				if newHeight < w.MinHeight() {
-					return
+				minHeight := w.title.height
+				if newHeight >= minHeight {
+					w.SetPositionY(w.Position().Y + delta)
+					w.SetHeight(math32.Max(newHeight, minHeight))
+				} else {
+					w.SetPositionY(w.Position().Y + w.Height() - minHeight)
+					w.SetHeight(w.title.height)
 				}
-				w.SetPositionY(w.Position().Y + delta)
-				w.SetHeight(newHeight)
-			case "right":
-				delta := cev.Xpos - w.mouseX
-				w.mouseX = cev.Xpos
+			}
+			if w.overRight {
+				delta := cev.Xpos - (w.pospix.X + w.width)
 				newWidth := w.Width() + delta
-				w.SetWidth(newWidth)
-			case "bottom":
-				delta := cev.Ypos - w.mouseY
-				w.mouseY = cev.Ypos
+				w.SetWidth(math32.Max(newWidth, w.title.label.Width() + w.title.closeButton.Width()))
+			}
+			if w.overBottom {
+				delta := cev.Ypos - (w.pospix.Y + w.height)
 				newHeight := w.Height() + delta
-				w.SetHeight(newHeight)
-			case "left":
-				delta := cev.Xpos - w.mouseX
-				w.mouseX = cev.Xpos
+				w.SetHeight(math32.Max(newHeight, w.title.height))
+			}
+			if w.overLeft {
+				delta := cev.Xpos - w.pospix.X
 				newWidth := w.Width() - delta
-				if newWidth < w.MinWidth() {
-					return
+				minWidth := w.title.label.Width() + w.title.closeButton.Width()
+				if newWidth >= minWidth {
+					w.SetPositionX(w.Position().X + delta)
+					w.SetWidth(math32.Max(newWidth, minWidth))
+				} else {
+					w.SetPositionX(w.Position().X + w.Width() - minWidth)
+					w.SetWidth(minWidth)
 				}
-				w.SetPositionX(w.Position().X + delta)
-				w.SetWidth(newWidth)
+			}
+		} else {
+			// Obtain cursor position relative to window
+			cx := cev.Xpos - w.pospix.X
+			cy := cev.Ypos - w.pospix.Y
+			// Check if cursor is on the top of the window (border + drag margin)
+			if cy <= w.borderSizes.Top {
+				w.overTop = true
+				w.root.SetCursorVResize()
+			} else {
+				w.overTop = false
+			}
+			// Check if cursor is on the bottom of the window (border + drag margin)
+			if cy >= w.height-w.borderSizes.Bottom - w.dragPadding {
+				w.overBottom = true
+			} else {
+				w.overBottom = false
+			}
+			// Check if cursor is on the left of the window (border + drag margin)
+			if cx <= w.borderSizes.Left + w.dragPadding {
+				w.overLeft = true
+				w.root.SetCursorHResize()
+			} else {
+				w.overLeft = false
+			}
+			// Check if cursor is on the right of the window (border + drag margin)
+			if cx >= w.width-w.borderSizes.Right - w.dragPadding {
+				w.overRight = true
+				w.root.SetCursorHResize()
+			} else {
+				w.overRight = false
+			}
+			// Update cursor image based on cursor position
+			if (w.overTop || w.overBottom) && !w.overRight && !w.overLeft {
+				w.root.SetCursorVResize()
+			} else if (w.overRight || w.overLeft) && !w.overTop && !w.overBottom {
+				w.root.SetCursorHResize()
+			} else if (w.overRight && w.overTop) || (w.overBottom && w.overLeft) {
+				w.root.SetCursorDiagResize1()
+			} else if (w.overRight && w.overBottom) || (w.overTop && w.overLeft) {
+				w.root.SetCursorDiagResize2()
+			}
+			// If cursor is not near the border of the window then reset the cursor
+			if !w.overTop && !w.overRight && !w.overBottom && !w.overLeft {
+				w.root.SetCursorNormal()
 			}
 		}
 	} else if evname == OnCursorLeave {
-		if !w.drag {
-			w.root.SetCursorNormal()
-		}
+		w.root.SetCursorNormal()
+		w.drag = false
 	}
 	w.root.StopPropagation(StopAll)
 }
 
-// update updates the button visual state
+// update updates the window's visual state.
 func (w *Window) update() {
 
 	if !w.Enabled() {
@@ -236,13 +261,16 @@ func (w *Window) update() {
 	w.applyStyle(&w.styles.Normal)
 }
 
+// applyStyle applies a window style to the window.
 func (w *Window) applyStyle(s *WindowStyle) {
 
 	w.SetBordersColor4(&s.BorderColor)
 	w.SetBordersFrom(&s.Border)
-	w.SetPaddingsFrom(&s.Paddings)
+	w.SetPaddingsFrom(&s.Padding)
+	w.client.SetMarginsFrom(&s.Margin)
+	w.client.SetColor4(&s.BgColor)
 	if w.title != nil {
-		w.title.applyStyle(s)
+		w.title.applyStyle(&s.TitleStyle)
 	}
 }
 
@@ -260,6 +288,7 @@ func (w *Window) recalc() {
 		w.title.recalc()
 		height -= w.title.height
 		cy = w.title.height
+
 	}
 
 	// Content area
@@ -269,16 +298,25 @@ func (w *Window) recalc() {
 
 // WindowTitle represents the title bar of a Window
 type WindowTitle struct {
-	Panel   // Embedded panel
-	win     *Window
-	label   Label
-	pressed bool
-	drag    bool
-	mouseX  float32
-	mouseY  float32
+	Panel              // Embedded panel
+	win                *Window // Window to which this title belongs
+	label              Label   // Label for the title
+	pressed            bool    // Whether the left mouse button is pressed
+	closeButton        *Button // The close button on the top right corner
+	closeButtonVisible bool    // Whether the close button is present
+
+	// Last mouse coordinates
+	mouseX             float32
+	mouseY             float32
+}
+
+// WindowTitleStyle contains the styling for a window title.
+type WindowTitleStyle struct {
+	PanelStyle
+	FgColor     math32.Color4
 }
 
-// newWindowTitle creates and returns a pointer to a window title panel
+// newWindowTitle creates and returns a pointer to a window title panel.
 func newWindowTitle(win *Window, text string) *WindowTitle {
 
 	wt := new(WindowTitle)
@@ -288,6 +326,19 @@ func newWindowTitle(win *Window, text string) *WindowTitle {
 	wt.label.initialize(text, StyleDefault().Font)
 	wt.Panel.Add(&wt.label)
 
+	wt.closeButton = NewButton("")
+	wt.closeButton.SetIcon(icon.Close)
+	wt.closeButton.Subscribe(OnCursorEnter, func(s string, i interface{}) {
+		wt.win.root.SetCursorNormal()
+	})
+	wt.closeButton.Subscribe(OnClick, func(s string, i interface{}) {
+		wt.win.Parent().GetNode().Remove(wt.win)
+		wt.win.Dispose()
+		wt.win.Dispatch("gui.OnWindowClose", nil)
+	})
+	wt.Panel.Add(wt.closeButton)
+	wt.closeButtonVisible = true
+
 	wt.Subscribe(OnMouseDown, wt.onMouse)
 	wt.Subscribe(OnMouseUp, wt.onMouse)
 	wt.Subscribe(OnCursor, wt.onCursor)
@@ -298,7 +349,19 @@ func newWindowTitle(win *Window, text string) *WindowTitle {
 	return wt
 }
 
-// onMouse process subscribed mouse button events over the window title
+// setCloseButton sets whether the close button is present on the top right corner.
+func (wt *WindowTitle) setCloseButton(state bool) {
+
+	if state {
+		wt.closeButtonVisible = true
+		wt.Panel.Add(wt.closeButton)
+	} else {
+		wt.closeButtonVisible = false
+		wt.Panel.Remove(wt.closeButton)
+	}
+}
+
+// onMouse process subscribed mouse button events over the window title.
 func (wt *WindowTitle) onMouse(evname string, ev interface{}) {
 
 	mev := ev.(*window.MouseEvent)
@@ -317,13 +380,12 @@ func (wt *WindowTitle) onMouse(evname string, ev interface{}) {
 	wt.win.root.StopPropagation(Stop3D)
 }
 
-// onCursor process subscribed cursor events over the window title
+// onCursor process subscribed cursor events over the window title.
 func (wt *WindowTitle) onCursor(evname string, ev interface{}) {
 
-	if evname == OnCursorEnter {
-		wt.win.root.SetCursorHand()
-	} else if evname == OnCursorLeave {
+	if evname == OnCursorLeave {
 		wt.win.root.SetCursorNormal()
+		wt.pressed = false
 	} else if evname == OnCursor {
 		if !wt.pressed {
 			wt.win.root.StopPropagation(Stop3D)
@@ -341,13 +403,11 @@ func (wt *WindowTitle) onCursor(evname string, ev interface{}) {
 	wt.win.root.StopPropagation(Stop3D)
 }
 
-// applyStyles sets the specified window title style
-func (wt *WindowTitle) applyStyle(s *WindowStyle) {
+// applyStyle applies the specified WindowTitleStyle.
+func (wt *WindowTitle) applyStyle(s *WindowTitleStyle) {
 
-	wt.SetBordersFrom(&s.TitleBorders)
-	wt.SetBordersColor4(&s.TitleBorderColor)
-	wt.SetColor4(&s.TitleBgColor)
-	wt.label.SetColor4(&s.TitleFgColor)
+	wt.Panel.ApplyStyle(&s.PanelStyle)
+	wt.label.SetColor4(&s.FgColor)
 }
 
 // recalc recalculates the height and position of the label in the title bar.
@@ -355,5 +415,9 @@ func (wt *WindowTitle) recalc() {
 
 	xpos := (wt.width - wt.label.width) / 2
 	wt.label.SetPositionX(xpos)
-	wt.SetContentHeight(wt.label.Height())
+	wt.SetContentHeight(wt.closeButton.Height())
+
+	if wt.closeButtonVisible {
+		wt.closeButton.SetPositionX(wt.width - wt.closeButton.width)
+	}
 }