diff --git a/ui/button.go b/ui/button.go index 8e917bb..a5b06e2 100644 --- a/ui/button.go +++ b/ui/button.go @@ -13,6 +13,8 @@ type Button struct { Icon string Text string Type ButtonType + + clicked ControlClickedEvents } type ButtonType int @@ -59,19 +61,48 @@ func (b *Button) icon(ctx Context) Texture { return ctx.Textures().Texture(b.Icon) } +func (b *Button) ButtonClicked() ControlClickedEventHandler { return &b.clicked } + func (b *Button) DesiredSize(ctx Context) geom.PointF32 { return b.desiredSize(ctx) } func (b *Button) Handle(ctx Context, e Event) bool { - result := b.ControlBase.Handle(ctx, e) + result := b.ControlBase.HandleNotify(ctx, e, b) if b.over { + if b.Disabled { + ctx.Renderer().SetMouseCursor(MouseCursorNotAllowed) + return true + } ctx.Renderer().SetMouseCursor(MouseCursorPointer) } return result } +func (b *Button) Notify(ctx Context, state interface{}) bool { + switch state.(type) { + case ControlClickedArgs: + if !b.Disabled { + if b.clicked.Notify(ctx, state) { + return true + } + } + } + return b.ControlBase.Notify(ctx, state) +} + func (b *Button) fillColor(p *Palette) color.Color { + if b.Disabled { + if b.Background != nil { + return p.Disabled + } + switch b.Type { + case ButtonTypeContained: + return p.Disabled + default: + return nil + } + } if b.Background != nil { if b.over && b.HoverColor != nil { return b.HoverColor @@ -100,6 +131,16 @@ func (b *Button) fillColor(p *Palette) color.Color { } func (b *Button) textColor(p *Palette) color.Color { + if b.Disabled { + if b.Background != nil { + return p.TextOnDisabled + } + switch b.Type { + case ButtonTypeContained: + return p.TextOnDisabled + } + return p.Disabled + } if b.Font.Color != nil { return b.Font.Color } @@ -151,6 +192,6 @@ func (b *Button) Render(ctx Context) { } if b.Type == ButtonTypeOutlined { - b.RenderOutline(ctx) + b.RenderOutlineDefault(ctx, textColor) } } diff --git a/ui/checkbox.go b/ui/checkbox.go index 683fc1b..45fd095 100644 --- a/ui/checkbox.go +++ b/ui/checkbox.go @@ -37,7 +37,7 @@ func (c *Checkbox) desiredSize(ctx Context) geom.PointF32 { func (c *Checkbox) icon(ctx Context) Texture { if c.Selected { return GetOrCreateIcon(ctx, "ui-default-checkbox-selected", c.selectedIcon) - } else if c.over { + } else if c.over && !c.Disabled { return GetOrCreateIcon(ctx, "ui-default-checkbox-hover", c.hoverIcon) } return c.getOrCreateNormalIcon(ctx) @@ -80,6 +80,10 @@ func (c *Checkbox) DesiredSize(ctx Context) geom.PointF32 { return c.desiredSize func (c *Checkbox) Handle(ctx Context, e Event) bool { result := c.ControlBase.Handle(ctx, e) if c.over { + if c.Disabled { + ctx.Renderer().SetMouseCursor(MouseCursorNotAllowed) + return true + } ctx.Renderer().SetMouseCursor(MouseCursorPointer) } if result { @@ -102,7 +106,7 @@ func (c *Checkbox) Render(ctx Context) { var style = ctx.Style() var palette = style.Palette - fore := c.FontColor(ctx) + fore := c.TextColor(ctx) bounds := c.bounds var pad = style.Dimensions.TextPadding @@ -112,8 +116,8 @@ func (c *Checkbox) Render(ctx Context) { icon := c.icon(ctx) if icon != nil { iconColor := fore - if c.Selected && c.Font.Color == nil { - iconColor = palette.Primary + if c.Selected { + iconColor = c.FontColor(ctx, palette.Primary) } scaledIcon, _ := ctx.Textures().ScaledHeight(icon, boundsH) // try to pre-scale icon if scaledIcon == nil { // let the renderer scale diff --git a/ui/control.go b/ui/control.go index f1ce14b..cc5ce84 100644 --- a/ui/control.go +++ b/ui/control.go @@ -11,6 +11,9 @@ type Control interface { Render(Context) Bounds() geom.RectangleF32 + Disable() + Enable() + IsDisabled() bool IsInBounds(p geom.PointF32) bool IsOver() bool Offset() geom.PointF32 diff --git a/ui/controlbase.go b/ui/controlbase.go index 65c12e7..8df78af 100644 --- a/ui/controlbase.go +++ b/ui/controlbase.go @@ -104,6 +104,8 @@ type ControlBase struct { Font FontStyle TextAlignment HorizontalAlignment + Disabled bool + Tooltip string } @@ -115,9 +117,23 @@ func (c *ControlBase) Arrange(ctx Context, bounds geom.RectangleF32, offset geom func (c *ControlBase) Bounds() geom.RectangleF32 { return c.bounds } +func (c *ControlBase) ControlClicked() ControlClickedEventHandler { return &c.clicked } + func (c *ControlBase) DesiredSize(Context) geom.PointF32 { return geom.ZeroPtF32 } -func (c *ControlBase) Handle(ctx Context, e Event) bool { +func (c *ControlBase) Disable() { c.Disabled = true } + +func (c *ControlBase) DragEnded() DragEndedEventHandler { return &c.dragEnded } + +func (c *ControlBase) DragMoved() DragMovedEventHandler { return &c.dragMoved } + +func (c *ControlBase) DragStarted() DragStartedEventHandler { return &c.dragStarted } + +func (c *ControlBase) Enable() { c.Disabled = false } + +func (c *ControlBase) Handle(ctx Context, e Event) bool { return c.HandleNotify(ctx, e, c) } + +func (c *ControlBase) HandleNotify(ctx Context, e Event, notifier Notifier) bool { defer func() { if c.Tooltip != "" && c.over { ctx.ShowTooltip(c.Tooltip) @@ -145,11 +161,11 @@ func (c *ControlBase) Handle(ctx Context, e Event) bool { if start, ok := c.drag.IsDragging(); ok { var move = c.ToControlPosition(e.Pos()) c.drag.Move(move) - return c.dragMoved.Notify(ctx, DragMovedArgs{Start: start, Current: move}) + return notifier.Notify(ctx, DragMovedArgs{Start: start, Current: move}) } var start = c.ToControlPosition(e.Pos()) c.drag.Start(start) - return c.dragStarted.Notify(ctx, DragStartedArgs{Start: start}) + return notifier.Notify(ctx, DragStartedArgs{Start: start}) } case *MouseLeaveEvent: c.over = false @@ -157,7 +173,7 @@ func (c *ControlBase) Handle(ctx Context, e Event) bool { c.over = over(e.MouseEvent) if c.over && e.Button == MouseButtonLeft { c.pressed = true - return c.clicked.Notify(ctx, ControlClickedArgs{Position: e.Pos(), Button: e.Button}) + return notifier.Notify(ctx, ControlClickedArgs{Position: e.Pos(), Button: e.Button}) } case *MouseButtonUpEvent: if e.Button == MouseButtonLeft { @@ -165,17 +181,20 @@ func (c *ControlBase) Handle(ctx Context, e Event) bool { if start, ok := c.drag.IsDragging(); ok { var end = c.ToControlPosition(e.Pos()) c.drag.Cancel() - return c.dragEnded.Notify(ctx, DragEndedArgs{Start: start, End: end}) + return notifier.Notify(ctx, DragEndedArgs{Start: start, End: end}) } } } return false } -func (c *ControlBase) FontColor(ctx Context) color.Color { +func (c *ControlBase) FontColor(ctx Context, color color.Color) color.Color { + if c.Disabled { + return ctx.Style().Palette.TextOnDisabled + } var text = c.Font.Color if text == nil { - text = ctx.Style().Palette.Text + text = color } return text } @@ -188,6 +207,8 @@ func (c *ControlBase) FontName(ctx Context) string { return name } +func (c *ControlBase) IsDisabled() bool { return c.Disabled } + func (c *ControlBase) IsInBounds(p geom.PointF32) bool { bounds := c.bounds if bounds.Min.X < 0 { @@ -201,19 +222,30 @@ func (c *ControlBase) IsInBounds(p geom.PointF32) bool { func (c *ControlBase) IsOver() bool { return c.over } -func (c *ControlBase) Parent() Control { return c.parent } - func (c *ControlBase) IsPressed() bool { return c.pressed } +func (c *ControlBase) Notify(ctx Context, state interface{}) bool { + switch state.(type) { + case ControlClickedArgs: + return c.clicked.Notify(ctx, state) + case DragEndedArgs: + return c.dragEnded.Notify(ctx, state) + case DragMovedArgs: + return c.dragMoved.Notify(ctx, state) + case DragStartedArgs: + return c.dragStarted.Notify(ctx, state) + default: + return false + } +} + +func (c *ControlBase) Parent() Control { return c.parent } + func (c *ControlBase) Offset() geom.PointF32 { return c.offset } -func (c *ControlBase) ControlClicked() ControlClickedEventHandler { return &c.clicked } - -func (c *ControlBase) DragEnded() DragEndedEventHandler { return &c.dragEnded } - -func (c *ControlBase) DragMoved() DragMovedEventHandler { return &c.dragMoved } - -func (c *ControlBase) DragStarted() DragStartedEventHandler { return &c.dragStarted } +func (c *ControlBase) OutlineColor(ctx Context) color.Color { + return c.FontColor(ctx, ctx.Style().Palette.Primary) +} func (c *ControlBase) Render(Context) {} @@ -224,13 +256,20 @@ func (c *ControlBase) RenderBackground(ctx Context) { } func (c *ControlBase) RenderOutline(ctx Context) { + c.RenderOutlineDefault(ctx, nil) +} + +func (c *ControlBase) RenderOutlineDefault(ctx Context, color color.Color) { style := ctx.Style() width := style.Dimensions.OutlineWidth - color := style.Palette.Primary - if c.Font.Color != nil { - color = c.Font.Color + if color == nil { + color = c.OutlineColor(ctx) } ctx.Renderer().Rectangle(c.bounds.Inset(.5*width), color, width) } +func (c *ControlBase) TextColor(ctx Context) color.Color { + return c.FontColor(ctx, ctx.Style().Palette.Text) +} + func (c *ControlBase) ToControlPosition(p geom.PointF32) geom.PointF32 { return p.Sub(c.offset) } diff --git a/ui/examples/01_basic/basic.go b/ui/examples/01_basic/basic.go index a77904b..11a0442 100644 --- a/ui/examples/01_basic/basic.go +++ b/ui/examples/01_basic/basic.go @@ -47,22 +47,62 @@ func (b *basic) Init(ctx ui.Context) error { stretch(ui.BuildIconButton("plus", "Text", func(b *ui.Button) { b.Type = ui.ButtonTypeText }), 8), } }), + ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) { + p.Children = []ui.Control{ + stretch(ui.BuildIconButton("plus", "Contained", func(b *ui.Button) { + b.Type = ui.ButtonTypeContained + b.Disabled = true + }), 8), + stretch(ui.BuildIconButton("plus", "Icon", func(b *ui.Button) { + b.Type = ui.ButtonTypeIcon + b.Disabled = true + }), 8), + stretch(ui.BuildIconButton("plus", "Outlined", func(b *ui.Button) { + b.Type = ui.ButtonTypeOutlined + b.Disabled = true + }), 8), + stretch(ui.BuildIconButton("plus", "Text", func(b *ui.Button) { + b.Type = ui.ButtonTypeText + b.Disabled = true + }), 8), + } + }), ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) { p.Children = []ui.Control{ &ui.Checkbox{}, ui.BuildCheckbox("Check me!", nil), } }), + ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) { + p.Children = []ui.Control{ + ui.BuildCheckbox("", func(b *ui.Checkbox) { b.Disabled = true }), + ui.BuildCheckbox("You can't check me!", func(b *ui.Checkbox) { + b.Selected = true + b.Disabled = true + }), + } + }), ui.Stretch(&ui.Label{Text: "Content"}), ui.Margin(ui.StretchWidth(ui.BuildTextBox(func(b *ui.TextBox) { b.Text = "Type here..." })), 8), + ui.Margin(ui.StretchWidth(ui.BuildTextBox(func(b *ui.TextBox) { + b.Text = "You can't type here..." + b.Disabled = true + })), 8), ui.Margin(ui.BuildButton("Quit", func(b *ui.Button) { - b.ControlClicked().AddHandler(func(ui.Context, ui.ControlClickedArgs) { + b.ButtonClicked().AddHandler(func(ui.Context, ui.ControlClickedArgs) { ctx.Quit() }) b.Tooltip = "Will quit the application" }), 8), + ui.Margin(ui.BuildButton("Can't quit", func(b *ui.Button) { + b.ButtonClicked().AddHandler(func(ui.Context, ui.ControlClickedArgs) { + ctx.Quit() + }) + b.Tooltip = "Will not quit the application" + b.Disabled = true + }), 8), ui.BuildLabel("Status...", func(l *ui.Label) { l.Background = style.Palette.PrimaryDark l.Font.Color = style.Palette.TextOnPrimary diff --git a/ui/label.go b/ui/label.go index 89d718f..44ef7b5 100644 --- a/ui/label.go +++ b/ui/label.go @@ -49,7 +49,7 @@ func (l *Label) DesiredSize(ctx Context) geom.PointF32 { func (l *Label) Render(ctx Context) { l.RenderBackground(ctx) - fontColor := l.FontColor(ctx) + fontColor := l.TextColor(ctx) fontName := l.FontName(ctx) pad := ctx.Style().Dimensions.TextPadding bounds := l.bounds.Inset(pad) diff --git a/ui/notifier.go b/ui/notifier.go new file mode 100644 index 0000000..5afc388 --- /dev/null +++ b/ui/notifier.go @@ -0,0 +1,5 @@ +package ui + +type Notifier interface { + Notify(Context, interface{}) bool +} diff --git a/ui/proxy.go b/ui/proxy.go index f64f28c..be6874b 100644 --- a/ui/proxy.go +++ b/ui/proxy.go @@ -28,6 +28,12 @@ func (p *Proxy) Bounds() geom.RectangleF32 { return p.Content.Bounds() } +func (p *Proxy) Disable() { p.Content.Disable() } + +func (p *Proxy) Enable() { p.Content.Enable() } + +func (p *Proxy) IsDisabled() bool { return p.Content.IsDisabled() } + func (p *Proxy) IsInBounds(pt geom.PointF32) bool { return p.Content.IsInBounds(pt) } func (p *Proxy) IsOver() bool { return p.Content.IsOver() } diff --git a/ui/slider.go b/ui/slider.go index bbc4dea..70c5dfc 100644 --- a/ui/slider.go +++ b/ui/slider.go @@ -165,6 +165,10 @@ func (h *sliderHandle) texture(ctx Context) Texture { func (h *sliderHandle) Handle(ctx Context, e Event) bool { h.ControlBase.Handle(ctx, e) if h.IsOver() { + if h.Disabled { + ctx.Renderer().SetMouseCursor(MouseCursorNotAllowed) + return true + } ctx.Renderer().SetMouseCursor(MouseCursorPointer) } return true diff --git a/ui/style.go b/ui/style.go index f8e3262..266db45 100644 --- a/ui/style.go +++ b/ui/style.go @@ -25,6 +25,8 @@ type FontNames struct { type Palette struct { // Background is the default background color. Background color.Color + // Disabled is the color to use when the control is disabled. + Disabled color.Color // Primary is used as a the main contrast color. Primary color.Color // PrimaryDark is a foreground version of the main contrast color. @@ -45,10 +47,10 @@ type Palette struct { ShadedBackground color.Color // Text is the default text color. Text color.Color - // TextDisabled is disabled text color. - TextDisabled color.Color // TextNegative is the text color associated with a negative event. TextNegative color.Color + // TextOnDisabled is text color rendered over an disabled control. + TextOnDisabled color.Color // TextOnPrimary is the text color when used with the main contrast color as background. TextOnPrimary color.Color // TextOnSecondary is the text color when used with the secondary contrast color as background. @@ -81,6 +83,7 @@ func DefaultFontNames() *FontNames { func DefaultPalette() *Palette { return &Palette{ Background: color.White, + Disabled: zntg.MustHexColor(`#BDBDBD`), Primary: zntg.MustHexColor(`#3F51B5`), PrimaryDark: zntg.MustHexColor(`#132584`), @@ -95,8 +98,8 @@ func DefaultPalette() *Palette { ShadedBackground: zntg.MustHexColor(`#FAFAFA`), Text: color.Black, - TextDisabled: zntg.MustHexColor(`#BDBDBD`), TextNegative: zntg.MustHexColor(`#FF4336`), + TextOnDisabled: zntg.MustHexColor(`#5C5C5C`), TextOnPrimary: color.White, TextOnSecondary: color.Black, TextPositive: zntg.MustHexColor(`#4CAF50`), diff --git a/ui/textbox.go b/ui/textbox.go index aab2a72..61abd0a 100644 --- a/ui/textbox.go +++ b/ui/textbox.go @@ -132,6 +132,10 @@ func (b *TextBox) Handle(ctx Context, e Event) bool { b.box.Handle(ctx, e) if b.over { + if b.Disabled { + ctx.Renderer().SetMouseCursor(MouseCursorNotAllowed) + return true + } ctx.Renderer().SetMouseCursor(MouseCursorText) } ctx.Animate() @@ -241,7 +245,7 @@ func (b *TextBox) Render(ctx Context) { b.RenderBackground(ctx) b.RenderOutline(ctx) - c := b.FontColor(ctx) + c := b.TextColor(ctx) style := ctx.Style() var caretWidth float32 = 1 b.box.RenderFn(ctx, func(_ Context, size geom.PointF32) {