Added Rect method to Control interface.
Fixed ContentScrollbar. Added horizontal alignment to label. Removed some dimensional constants. Added several controls: - Margin; - Button; - Columns; - Scroll; - Wrapper (reusable layout control) that wraps around existing control.
This commit is contained in:
parent
eb0b660ab6
commit
9d4b097352
54
ui/button.go
Normal file
54
ui/button.go
Normal file
@ -0,0 +1,54 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/galleg/allegro5"
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Button struct {
|
||||
ControlBase
|
||||
Text string
|
||||
HorizontalAlignment allegro5.HorizontalAlignment
|
||||
}
|
||||
|
||||
func NewButton(text string, click MouseClickFn) *Button {
|
||||
return &Button{ControlBase: ControlBase{OnClick: click}, Text: text}
|
||||
}
|
||||
|
||||
func NewButtonAlign(text string, click MouseClickFn, align allegro5.HorizontalAlignment) *Button {
|
||||
return &Button{ControlBase: ControlBase{OnClick: click}, Text: text, HorizontalAlignment: align}
|
||||
}
|
||||
|
||||
func (b *Button) DesiredSize(ctx Context) geom.PointF {
|
||||
var fonts = ctx.Fonts()
|
||||
var fnt = fonts.Get("default")
|
||||
var w = fnt.TextWidth(b.Text)
|
||||
var fntH = fnt.Height()
|
||||
return geom.PtF(float64(w+fntH), float64(2*fntH))
|
||||
}
|
||||
|
||||
func (b *Button) Render(ctx Context) {
|
||||
var fonts = ctx.Fonts()
|
||||
|
||||
var min = b.Bounds.Min.To32()
|
||||
var max = b.Bounds.Max.To32()
|
||||
|
||||
var fnt = fonts.Get("default")
|
||||
var fntH = fnt.Height()
|
||||
|
||||
var back = ctx.Palette().Primary()
|
||||
if b.IsOver && !b.IsPressed {
|
||||
back = ctx.Palette().PrimaryHighlight()
|
||||
}
|
||||
allegro5.DrawFilledRectangle(min.X, min.Y, max.X, max.Y, back)
|
||||
switch b.HorizontalAlignment {
|
||||
case allegro5.AlignLeft:
|
||||
fnt.Draw(min.X+.5*fntH, min.Y+.5*fntH, ctx.Palette().Lightest(), allegro5.AlignLeft, b.Text)
|
||||
case allegro5.AlignCenter:
|
||||
fnt.Draw(.5*(min.X+max.X), min.Y+.5*fntH, ctx.Palette().Lightest(), allegro5.AlignCenter, b.Text)
|
||||
case allegro5.AlignRight:
|
||||
fnt.Draw(min.X-.5*fntH, min.Y+.5*fntH, ctx.Palette().Lightest(), allegro5.AlignRight, b.Text)
|
||||
}
|
||||
|
||||
b.ControlBase.Render(ctx)
|
||||
}
|
@ -84,7 +84,7 @@ func (c *Checkbox) DesiredSize(ctx Context) geom.PointF {
|
||||
var fonts = ctx.Fonts()
|
||||
var fnt = fonts.Get("default")
|
||||
var w = fnt.TextWidth(c.Text)
|
||||
return geom.PtF(float64(w+checkboxSize+leftMargin), checkboxSize)
|
||||
return geom.PtF(float64(w+checkboxSize), checkboxSize)
|
||||
}
|
||||
|
||||
func (c *Checkbox) box() *allegro5.Bitmap {
|
||||
|
114
ui/columns.go
Normal file
114
ui/columns.go
Normal file
@ -0,0 +1,114 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type columns struct {
|
||||
ContainerBase
|
||||
cols []*column
|
||||
col int
|
||||
}
|
||||
|
||||
type column struct {
|
||||
p DockPanel
|
||||
}
|
||||
|
||||
func (c *column) Append(ctrl Control) {
|
||||
c.p.Append(DockTop, ctrl)
|
||||
}
|
||||
|
||||
func (c *column) AppendCreate(ctx Context, ctrl Control) {
|
||||
c.p.AppendCreate(ctx, DockTop, ctrl)
|
||||
}
|
||||
|
||||
type Columns interface {
|
||||
Container
|
||||
Columns() []Column
|
||||
Append(Control)
|
||||
AppendCreate(Context, Control)
|
||||
}
|
||||
|
||||
type Column interface {
|
||||
Append(Control)
|
||||
AppendCreate(Context, Control)
|
||||
}
|
||||
|
||||
func NewColumns(cols int, ctrls ...Control) Columns {
|
||||
var c = &columns{}
|
||||
for i := 0; i < cols; i++ {
|
||||
c.addColumn()
|
||||
}
|
||||
for _, ctrl := range ctrls {
|
||||
c.Append(ctrl)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *columns) addColumn() {
|
||||
var p = NewDockPanel(c)
|
||||
c.ContainerBase.Append(p)
|
||||
c.cols = append(c.cols, &column{p})
|
||||
}
|
||||
|
||||
func (c *columns) next() {
|
||||
c.col = (c.col + 1) % len(c.cols)
|
||||
}
|
||||
|
||||
func (c *columns) Columns() []Column {
|
||||
var cols = make([]Column, len(c.cols))
|
||||
for i, col := range c.cols {
|
||||
cols[i] = col
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
func (c *columns) Append(ctrl Control) {
|
||||
c.cols[c.col].Append(ctrl)
|
||||
c.next()
|
||||
}
|
||||
|
||||
func (c *columns) AppendCreate(ctx Context, ctrl Control) {
|
||||
c.cols[c.col].AppendCreate(ctx, ctrl)
|
||||
c.next()
|
||||
}
|
||||
|
||||
func (c *columns) DesiredSize(ctx Context) geom.PointF {
|
||||
var w float64
|
||||
var h float64
|
||||
for _, col := range c.cols {
|
||||
var sz = col.p.DesiredSize(ctx)
|
||||
if !math.IsNaN(w) {
|
||||
if math.IsNaN(sz.X) {
|
||||
w = sz.X
|
||||
} else {
|
||||
w += sz.X
|
||||
}
|
||||
}
|
||||
if !math.IsNaN(h) {
|
||||
if math.IsNaN(sz.Y) {
|
||||
h = sz.Y
|
||||
} else {
|
||||
h = math.Max(h, sz.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
return geom.PtF(w, h)
|
||||
}
|
||||
|
||||
func (c *columns) Arrange(ctx Context, rect geom.RectangleF) {
|
||||
c.ContainerBase.SetRect(rect)
|
||||
var w, h = rect.Dx(), rect.Dy()
|
||||
var cols = float64(len(c.cols))
|
||||
for i, col := range c.cols {
|
||||
var ii = float64(i)
|
||||
var colH = col.p.DesiredSize(ctx).Y
|
||||
if colH > h {
|
||||
colH = h
|
||||
}
|
||||
var colR = geom.RectF(rect.Min.X+ii*w/cols, rect.Min.Y, rect.Min.X+(ii+1)*w/cols, rect.Min.Y+colH)
|
||||
Arrange(ctx, col.p, colR)
|
||||
}
|
||||
}
|
@ -18,6 +18,8 @@ type ContentScrollbar struct {
|
||||
Orientation Orientation
|
||||
OnChanged ContentScrollbarValueChangedFn
|
||||
handle *contentScrollbarHandle
|
||||
barLength float64
|
||||
scrollDistance float64
|
||||
}
|
||||
|
||||
type contentScrollbarHandle struct {
|
||||
@ -47,17 +49,21 @@ func (s *ContentScrollbar) Destroyed(ctx Context) {
|
||||
s.handle.Destroyed(ctx)
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) barViewRange() (float64, float64) {
|
||||
func (s *ContentScrollbar) length() (float64, float64) {
|
||||
var min, max float64
|
||||
switch s.Orientation {
|
||||
case OrientationHorizontal:
|
||||
min = s.Bounds.Min.X
|
||||
max = s.Bounds.Max.X
|
||||
default:
|
||||
min = s.Bounds.Max.Y
|
||||
max = s.Bounds.Min.Y
|
||||
min = s.Bounds.Min.Y
|
||||
max = s.Bounds.Max.Y
|
||||
}
|
||||
var length = (max - min)
|
||||
return max - min, min
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) updateBarLength() {
|
||||
var length, _ = s.length()
|
||||
var bar = length
|
||||
if s.Length > length {
|
||||
bar = length * length / s.Length
|
||||
@ -69,7 +75,12 @@ func (s *ContentScrollbar) barViewRange() (float64, float64) {
|
||||
bar = 20
|
||||
}
|
||||
}
|
||||
return min + .5*bar, max - .5*bar
|
||||
s.barLength = bar
|
||||
var d = s.Length - length
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
s.scrollDistance = d
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) barViewCenter() float64 {
|
||||
@ -82,23 +93,21 @@ func (s *ContentScrollbar) barViewCenter() float64 {
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) toValue(x, y float64) float64 {
|
||||
var n = y
|
||||
var pos = y
|
||||
if OrientationHorizontal == s.Orientation {
|
||||
n = x
|
||||
pos = x
|
||||
}
|
||||
var min, max = s.barViewRange()
|
||||
if min == max {
|
||||
var length, min = s.length()
|
||||
if length == 0 {
|
||||
return 0
|
||||
}
|
||||
var delta = s.Length - (max - min)
|
||||
var off = (n - min) / (max - min)
|
||||
var v = off * (s.Length - (max - min))
|
||||
if v < 0 {
|
||||
v = 0
|
||||
} else if v > delta {
|
||||
v = delta
|
||||
var offset = (pos - .5*s.barLength - min) / (length - s.barLength)
|
||||
if offset < 0 {
|
||||
return 0
|
||||
} else if offset > 1 {
|
||||
return s.scrollDistance
|
||||
}
|
||||
return v
|
||||
return s.scrollDistance * offset
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) change(v float64) {
|
||||
@ -117,13 +126,16 @@ func (s *ContentScrollbar) snapTo(x, y int) {
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) increment(d int) {
|
||||
// var val = s.Value + d
|
||||
// if val < s.Minimum {
|
||||
// val = s.Minimum
|
||||
// } else if val > s.Maximum {
|
||||
// val = s.Maximum
|
||||
// }
|
||||
// s.change(val)
|
||||
if s.Orientation == OrientationVertical {
|
||||
d *= -1
|
||||
}
|
||||
var val = s.Value + float64(d)*ScrollbarWidth
|
||||
if val < 0 {
|
||||
val = 0
|
||||
} else if val > s.scrollDistance {
|
||||
val = s.scrollDistance
|
||||
}
|
||||
s.change(val)
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) Handle(ctx Context, ev allegro5.Event) {
|
||||
@ -168,19 +180,21 @@ func (s *ContentScrollbar) SetRect(rect geom.RectangleF) {
|
||||
}
|
||||
}
|
||||
s.ControlBase.SetRect(rect)
|
||||
s.updateBarLength()
|
||||
|
||||
// var min, max = s.barViewRange()
|
||||
// var off = float64(s.Value-s.Minimum) / float64(s.Maximum-s.Minimum)
|
||||
// var centerH = min + (max-min)*off
|
||||
// var r = 0.5 * float64(s.handles[0].Width())
|
||||
|
||||
// var center = s.barViewCenter()
|
||||
// switch s.Orientation {
|
||||
// case OrientationHorizontal:
|
||||
// s.handle.SetRect(geom.RectF(centerH-r, center-r, centerH+r, center+r))
|
||||
// default:
|
||||
// s.handle.SetRect(geom.RectF(center-r, centerH-r, center+r, centerH+r))
|
||||
// }
|
||||
var offset float64
|
||||
if 0 < s.scrollDistance {
|
||||
offset = s.Value / s.scrollDistance
|
||||
}
|
||||
var length, min = s.length()
|
||||
var begin = min + (length-s.barLength)*offset
|
||||
var end = begin + s.barLength
|
||||
switch s.Orientation {
|
||||
case OrientationHorizontal:
|
||||
s.handle.SetRect(geom.RectF(begin, rect.Min.Y, end, rect.Max.Y))
|
||||
default:
|
||||
s.handle.SetRect(geom.RectF(rect.Min.X, begin, rect.Max.X, end))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ContentScrollbar) Render(ctx Context) {
|
||||
|
@ -14,6 +14,7 @@ type Control interface {
|
||||
Update(Context, time.Duration)
|
||||
Handle(Context, allegro5.Event)
|
||||
DesiredSize(Context) geom.PointF
|
||||
Rect() geom.RectangleF
|
||||
SetRect(geom.RectangleF)
|
||||
Render(Context)
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package ui
|
||||
|
||||
const topMargin = 4
|
||||
const leftMargin = 8
|
||||
const lineHeight = 16
|
||||
const checkboxMargin = 4
|
||||
const checkboxSize = 24
|
||||
|
@ -30,6 +30,14 @@ func NewDockPanel(parent Container) DockPanel {
|
||||
return &dockPanel{ContainerBase: ContainerBase{parent: parent}}
|
||||
}
|
||||
|
||||
func NewDockPanelContent(parent Container, d Dock, controls ...Control) DockPanel {
|
||||
var p = NewDockPanel(parent)
|
||||
for _, c := range controls {
|
||||
p.Append(d, c)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *dockPanel) Append(d Dock, children ...Control) error {
|
||||
for _, child := range children {
|
||||
p.ContainerBase.Append(child)
|
||||
|
15
ui/label.go
15
ui/label.go
@ -1,10 +1,21 @@
|
||||
package ui
|
||||
|
||||
import "opslag.de/schobers/galleg/allegro5"
|
||||
import (
|
||||
"opslag.de/schobers/galleg/allegro5"
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Label struct {
|
||||
ControlBase
|
||||
Text string
|
||||
HorizontalAlignment allegro5.HorizontalAlignment
|
||||
}
|
||||
|
||||
func (l *Label) DesiredSize(ctx Context) geom.PointF {
|
||||
var fonts = ctx.Fonts()
|
||||
var fnt = fonts.Get("default")
|
||||
var _, _, w, h = fnt.TextDimensions(l.Text)
|
||||
return geom.PtF(float64(w), float64(h))
|
||||
}
|
||||
|
||||
func (l *Label) Render(ctx Context) {
|
||||
@ -13,7 +24,7 @@ func (l *Label) Render(ctx Context) {
|
||||
var min = l.Bounds.Min.To32()
|
||||
|
||||
var fnt = fonts.Get("default")
|
||||
fnt.Draw(min.X+leftMargin, min.Y+lineHeight-fnt.Ascent(), ctx.Palette().Darkest(), allegro5.AlignLeft, l.Text)
|
||||
fnt.Draw(min.X, min.Y+fnt.Height()-fnt.Ascent(), ctx.Palette().Darkest(), l.HorizontalAlignment, l.Text)
|
||||
|
||||
l.ControlBase.Render(ctx)
|
||||
}
|
||||
|
50
ui/margin.go
Normal file
50
ui/margin.go
Normal file
@ -0,0 +1,50 @@
|
||||
package ui
|
||||
|
||||
import "opslag.de/schobers/geom"
|
||||
|
||||
type margin struct {
|
||||
Wrapper
|
||||
Left, Top, Right, Bottom float64
|
||||
}
|
||||
|
||||
func Margin(c Control, m float64) Control {
|
||||
return &margin{Wrap(c), m, m, m, m}
|
||||
}
|
||||
|
||||
func LeftMargin(c Control, m float64) Control {
|
||||
return &margin{Wrap(c), m, 0, 0, 0}
|
||||
}
|
||||
|
||||
func TopMargin(c Control, m float64) Control {
|
||||
return &margin{Wrap(c), 0, m, 0, 0}
|
||||
}
|
||||
|
||||
func HorizontalMargin(c Control, m float64) Control {
|
||||
return &margin{Wrap(c), m, 0, m, 0}
|
||||
}
|
||||
|
||||
func VerticalMargin(c Control, m float64) Control {
|
||||
return &margin{Wrap(c), 0, m, 0, m}
|
||||
}
|
||||
|
||||
func (m *margin) DesiredSize(ctx Context) geom.PointF {
|
||||
var sz = m.Wrapper.DesiredSize(ctx)
|
||||
return geom.PtF(sz.X+m.Left+m.Right, sz.Y+m.Top+m.Bottom)
|
||||
}
|
||||
|
||||
func (m *margin) Arrange(ctx Context, rect geom.RectangleF) {
|
||||
m.Wrapper.SetRect(rect)
|
||||
rect.Min.X += m.Left
|
||||
rect.Min.Y += m.Top
|
||||
rect.Max.X -= m.Right
|
||||
rect.Max.Y -= m.Bottom
|
||||
if rect.Min.X > rect.Max.X {
|
||||
var x = .5 * (rect.Min.X + rect.Max.X)
|
||||
rect.Min.X, rect.Max.X = x, x
|
||||
}
|
||||
if rect.Min.Y > rect.Max.Y {
|
||||
var y = .5 * (rect.Min.Y + rect.Max.Y)
|
||||
rect.Min.Y, rect.Max.Y = y, y
|
||||
}
|
||||
Arrange(ctx, m.Wrapped, rect)
|
||||
}
|
77
ui/scroll.go
Normal file
77
ui/scroll.go
Normal file
@ -0,0 +1,77 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"opslag.de/schobers/galleg/allegro5"
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type scroll struct {
|
||||
Wrapper
|
||||
Content Control
|
||||
Bar *ContentScrollbar
|
||||
}
|
||||
|
||||
func Scroll(c Control, o Orientation) Control {
|
||||
var dock = NewDockPanel(nil)
|
||||
var bar = &ContentScrollbar{Orientation: o}
|
||||
switch o {
|
||||
case OrientationHorizontal:
|
||||
dock.Append(DockBottom, TopMargin(bar, ScrollbarWidth))
|
||||
dock.Append(DockBottom, c)
|
||||
case OrientationVertical:
|
||||
dock.Append(DockRight, LeftMargin(bar, ScrollbarWidth))
|
||||
dock.Append(DockRight, c)
|
||||
}
|
||||
var s = &scroll{Wrap(dock), c, bar}
|
||||
bar.OnChanged = func(v float64) {
|
||||
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *scroll) Handle(ctx Context, ev allegro5.Event) {
|
||||
s.Wrapper.Handle(ctx, ev)
|
||||
switch e := ev.(type) {
|
||||
case *allegro5.MouseMoveEvent:
|
||||
if 0 != e.DeltaZ && !s.Bar.IsOver && geom.PtF(float64(e.X), float64(e.Y)).In(s.Bounds) {
|
||||
var d = e.DeltaZ
|
||||
if allegro5.IsAnyKeyDown(allegro5.KeyLShift, allegro5.KeyRShift) {
|
||||
d *= 10
|
||||
}
|
||||
s.Bar.increment(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scroll) Render(ctx Context) {
|
||||
var bounds = s.Content.Rect()
|
||||
var w, h = bounds.Dx(), bounds.Dy()
|
||||
var bmp, err = allegro5.NewVideoBitmap(int(w), int(h))
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
defer bmp.Destroy()
|
||||
bmp.SetAsTarget()
|
||||
switch s.Bar.Orientation {
|
||||
case OrientationHorizontal:
|
||||
Arrange(ctx, s.Content, geom.RectF(-s.Bar.Value, 0, w, h))
|
||||
case OrientationVertical:
|
||||
Arrange(ctx, s.Content, geom.RectF(0, -s.Bar.Value, w, h))
|
||||
}
|
||||
s.Content.Render(ctx)
|
||||
ctx.Display().SetAsTarget()
|
||||
var min = s.Bounds.Min.To32()
|
||||
bmp.Draw(min.X, min.Y)
|
||||
s.Bar.Render(ctx)
|
||||
}
|
||||
|
||||
func (s *scroll) Arrange(ctx Context, rect geom.RectangleF) {
|
||||
var sz = s.Content.DesiredSize(ctx)
|
||||
switch s.Bar.Orientation {
|
||||
case OrientationHorizontal:
|
||||
s.Bar.Length = sz.X
|
||||
case OrientationVertical:
|
||||
s.Bar.Length = sz.Y
|
||||
}
|
||||
s.Wrapper.Arrange(ctx, rect)
|
||||
}
|
@ -18,6 +18,7 @@ type State interface {
|
||||
|
||||
type StateBase struct {
|
||||
Control Control
|
||||
ChangeTo State
|
||||
}
|
||||
|
||||
func (s *StateBase) Enter(ctx Context) error {
|
||||
@ -42,7 +43,7 @@ func (s *StateBase) Update(ctx Context, dt time.Duration) (State, error) {
|
||||
if nil != s.Control {
|
||||
s.Control.Update(ctx, dt)
|
||||
}
|
||||
return nil, nil
|
||||
return s.ChangeTo, nil
|
||||
}
|
||||
|
||||
func (s *StateBase) Handle(ctx Context, ev allegro5.Event) error {
|
||||
|
58
ui/wrapper.go
Normal file
58
ui/wrapper.go
Normal file
@ -0,0 +1,58 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"opslag.de/schobers/galleg/allegro5"
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
type Wrapper struct {
|
||||
Wrapped Control
|
||||
Bounds geom.RectangleF
|
||||
}
|
||||
|
||||
func Wrap(c Control) Wrapper {
|
||||
return Wrapper{Wrapped: c}
|
||||
}
|
||||
|
||||
func (w *Wrapper) Created(ctx Context, p Container) error {
|
||||
return w.Wrapped.Created(ctx, p)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Destroyed(ctx Context) {
|
||||
w.Wrapped.Destroyed(ctx)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Update(ctx Context, d time.Duration) {
|
||||
w.Wrapped.Update(ctx, d)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Handle(ctx Context, ev allegro5.Event) {
|
||||
w.Wrapped.Handle(ctx, ev)
|
||||
}
|
||||
|
||||
func (w *Wrapper) DesiredSize(ctx Context) geom.PointF {
|
||||
return w.Wrapped.DesiredSize(ctx)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Rect() geom.RectangleF {
|
||||
return w.Bounds
|
||||
}
|
||||
|
||||
func (w *Wrapper) SetRect(rect geom.RectangleF) {
|
||||
w.Bounds = rect
|
||||
}
|
||||
|
||||
func (w *Wrapper) Render(ctx Context) {
|
||||
w.Wrapped.Render(ctx)
|
||||
}
|
||||
|
||||
func (w *Wrapper) Children() []Control {
|
||||
return []Control{w.Wrapped}
|
||||
}
|
||||
|
||||
func (w *Wrapper) Arrange(ctx Context, rect geom.RectangleF) {
|
||||
w.Bounds = rect
|
||||
Arrange(ctx, w.Wrapped, rect)
|
||||
}
|
Loading…
Reference in New Issue
Block a user