mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 18:35:34 +00:00
widget: replace segmentIterator with simpler functions
The replacement functions all use the single seeking function, seekPosition. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+77
-48
@@ -61,7 +61,6 @@ type Editor struct {
|
|||||||
viewSize image.Point
|
viewSize image.Point
|
||||||
valid bool
|
valid bool
|
||||||
lines []text.Line
|
lines []text.Line
|
||||||
shapes []line
|
|
||||||
dims layout.Dimensions
|
dims layout.Dimensions
|
||||||
requestFocus bool
|
requestFocus bool
|
||||||
|
|
||||||
@@ -620,34 +619,6 @@ func (e *Editor) layout(gtx layout.Context, content layout.Widget) layout.Dimens
|
|||||||
e.scrollToCaret()
|
e.scrollToCaret()
|
||||||
}
|
}
|
||||||
|
|
||||||
off := image.Point{
|
|
||||||
X: -e.scrollOff.X,
|
|
||||||
Y: -e.scrollOff.Y,
|
|
||||||
}
|
|
||||||
cl := textPadding(e.lines)
|
|
||||||
cl.Max = cl.Max.Add(e.viewSize)
|
|
||||||
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
|
|
||||||
caretEnd := e.closestPosition(combinedPos{runes: e.caret.end})
|
|
||||||
startSel, endSel := sortPoints(caretStart.lineCol, caretEnd.lineCol)
|
|
||||||
it := segmentIterator{
|
|
||||||
startSel: startSel,
|
|
||||||
endSel: endSel,
|
|
||||||
Lines: e.lines,
|
|
||||||
Clip: cl,
|
|
||||||
Alignment: e.Alignment,
|
|
||||||
Width: e.viewSize.X,
|
|
||||||
Offset: off,
|
|
||||||
}
|
|
||||||
e.shapes = e.shapes[:0]
|
|
||||||
for {
|
|
||||||
layout, off, selected, yOffs, size, ok := it.Next()
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
op := clip.Outline{Path: e.shaper.Shape(e.font, e.textSize, layout)}.Op()
|
|
||||||
e.shapes = append(e.shapes, line{off, op, selected, yOffs, size})
|
|
||||||
}
|
|
||||||
|
|
||||||
key.InputOp{Tag: &e.eventKey, Hint: e.InputHint}.Add(gtx.Ops)
|
key.InputOp{Tag: &e.eventKey, Hint: e.InputHint}.Add(gtx.Ops)
|
||||||
if e.requestFocus {
|
if e.requestFocus {
|
||||||
key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
|
||||||
@@ -700,17 +671,50 @@ func (e *Editor) PaintSelection(gtx layout.Context) {
|
|||||||
cl := textPadding(e.lines)
|
cl := textPadding(e.lines)
|
||||||
cl.Max = cl.Max.Add(e.viewSize)
|
cl.Max = cl.Max.Add(e.viewSize)
|
||||||
defer clip.Rect(cl).Push(gtx.Ops).Pop()
|
defer clip.Rect(cl).Push(gtx.Ops).Pop()
|
||||||
for _, shape := range e.shapes {
|
selStart, selEnd := e.caret.start, e.caret.end
|
||||||
if !shape.selected {
|
if selStart > selEnd {
|
||||||
continue
|
selStart, selEnd = selEnd, selStart
|
||||||
}
|
}
|
||||||
offset := shape.offset
|
caretStart := e.closestPosition(combinedPos{runes: selStart})
|
||||||
offset.Y += shape.selectionYOffs
|
caretEnd := e.closestPosition(combinedPos{runes: selEnd})
|
||||||
t := op.Offset(layout.FPt(offset)).Push(gtx.Ops)
|
scroll := image.Point{
|
||||||
cl := clip.Rect(image.Rectangle{Max: shape.selectionSize}).Push(gtx.Ops)
|
X: e.scrollOff.X,
|
||||||
|
Y: e.scrollOff.Y,
|
||||||
|
}
|
||||||
|
cl = cl.Add(scroll)
|
||||||
|
pos := e.seekFirstVisibleLine(cl.Min.Y)
|
||||||
|
for !posIsBelow(e.lines, pos, cl.Max.Y) {
|
||||||
|
start, end := clipLine(e.lines, e.Alignment, e.viewSize.X, cl, pos)
|
||||||
|
lineIdx := start.lineCol.Y
|
||||||
|
if lineIdx > caretEnd.lineCol.Y {
|
||||||
|
// Line is after selection end; we're done.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Clamp start, end to selection.
|
||||||
|
if start.runes < selStart {
|
||||||
|
start = caretStart
|
||||||
|
}
|
||||||
|
if end.runes > selEnd {
|
||||||
|
end = caretEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
line := e.lines[start.lineCol.Y]
|
||||||
|
dotStart := image.Pt(start.x.Round(), start.y)
|
||||||
|
dotEnd := image.Pt(end.x.Round(), end.y)
|
||||||
|
t := op.Offset(layout.FPt(scroll.Mul(-1))).Push(gtx.Ops)
|
||||||
|
size := image.Rectangle{
|
||||||
|
Min: dotStart.Sub(image.Point{Y: line.Ascent.Ceil()}),
|
||||||
|
Max: dotEnd.Add(image.Point{Y: line.Descent.Ceil()}),
|
||||||
|
}
|
||||||
|
op := clip.Rect(size).Push(gtx.Ops)
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
paint.PaintOp{}.Add(gtx.Ops)
|
||||||
cl.Pop()
|
op.Pop()
|
||||||
t.Pop()
|
t.Pop()
|
||||||
|
|
||||||
|
if pos.lineCol.Y == len(e.lines)-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos = e.closestPosition(combinedPos{lineCol: screenPos{Y: pos.lineCol.Y + 1}})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,12 +722,28 @@ func (e *Editor) PaintText(gtx layout.Context) {
|
|||||||
cl := textPadding(e.lines)
|
cl := textPadding(e.lines)
|
||||||
cl.Max = cl.Max.Add(e.viewSize)
|
cl.Max = cl.Max.Add(e.viewSize)
|
||||||
defer clip.Rect(cl).Push(gtx.Ops).Pop()
|
defer clip.Rect(cl).Push(gtx.Ops).Pop()
|
||||||
for _, shape := range e.shapes {
|
scroll := image.Point{
|
||||||
t := op.Offset(layout.FPt(shape.offset)).Push(gtx.Ops)
|
X: e.scrollOff.X,
|
||||||
cl := shape.clip.Push(gtx.Ops)
|
Y: e.scrollOff.Y,
|
||||||
|
}
|
||||||
|
cl = cl.Add(scroll)
|
||||||
|
pos := e.seekFirstVisibleLine(cl.Min.Y)
|
||||||
|
for !posIsBelow(e.lines, pos, cl.Max.Y) {
|
||||||
|
start, end := clipLine(e.lines, e.Alignment, e.viewSize.X, cl, pos)
|
||||||
|
line := e.lines[start.lineCol.Y]
|
||||||
|
l := subLayout(line, start.lineCol.X, end.lineCol.X)
|
||||||
|
|
||||||
|
off := image.Point{X: start.x.Floor(), Y: start.y}.Sub(scroll)
|
||||||
|
t := op.Offset(layout.FPt(off)).Push(gtx.Ops)
|
||||||
|
op := clip.Outline{Path: e.shaper.Shape(e.font, e.textSize, l)}.Op().Push(gtx.Ops)
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
paint.PaintOp{}.Add(gtx.Ops)
|
||||||
cl.Pop()
|
op.Pop()
|
||||||
t.Pop()
|
t.Pop()
|
||||||
|
|
||||||
|
if pos.lineCol.Y == len(e.lines)-1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos = e.closestPosition(combinedPos{lineCol: screenPos{Y: pos.lineCol.Y + 1}})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +777,19 @@ func (e *Editor) PaintCaret(gtx layout.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Editor) seekFirstVisibleLine(y int) combinedPos {
|
||||||
|
pos := e.closestPosition(combinedPos{y: y})
|
||||||
|
for pos.lineCol.Y > 0 {
|
||||||
|
prevLine := pos.lineCol.Y - 1
|
||||||
|
prev := e.closestPosition(combinedPos{lineCol: screenPos{Y: prevLine}})
|
||||||
|
if posIsAbove(e.lines, prev, y) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos = prev
|
||||||
|
}
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Editor) caretInfo() (pos image.Point, ascent, descent int) {
|
func (e *Editor) caretInfo() (pos image.Point, ascent, descent int) {
|
||||||
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
|
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
|
||||||
carX := caretStart.x
|
carX := caretStart.x
|
||||||
@@ -889,11 +922,7 @@ func (e *Editor) indexPosition(pos combinedPos) combinedPos {
|
|||||||
e.makeValid()
|
e.makeValid()
|
||||||
// Initialize index with first caret position.
|
// Initialize index with first caret position.
|
||||||
if len(e.index) == 0 {
|
if len(e.index) == 0 {
|
||||||
l := e.lines[0]
|
e.index = append(e.index, firstPos(e.lines[0], e.Alignment, e.viewSize.X))
|
||||||
e.index = append(e.index, combinedPos{
|
|
||||||
x: align(e.Alignment, l.Width, e.viewSize.X),
|
|
||||||
y: l.Ascent.Ceil(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
i := sort.Search(len(e.index), func(i int) bool {
|
i := sort.Search(len(e.index), func(i int) bool {
|
||||||
return positionGreaterOrEqual(e.lines, e.index[i], pos)
|
return positionGreaterOrEqual(e.lines, e.index[i], pos)
|
||||||
@@ -949,7 +978,7 @@ func (e *Editor) closestPosition(pos combinedPos) combinedPos {
|
|||||||
const runesPerIndexEntry = 50
|
const runesPerIndexEntry = 50
|
||||||
for {
|
for {
|
||||||
var done bool
|
var done bool
|
||||||
closest, done = seekPosition(e.lines, closest, pos, e.Alignment, e.viewSize.X, runesPerIndexEntry)
|
closest, done = seekPosition(e.lines, e.Alignment, e.viewSize.X, closest, pos, runesPerIndexEntry)
|
||||||
if done {
|
if done {
|
||||||
return closest
|
return closest
|
||||||
}
|
}
|
||||||
@@ -959,7 +988,7 @@ func (e *Editor) closestPosition(pos combinedPos) combinedPos {
|
|||||||
|
|
||||||
// seekPosition seeks to the position closest to needle, starting at start and returns true.
|
// seekPosition seeks to the position closest to needle, starting at start and returns true.
|
||||||
// If limit is non-zero, seekPosition stops seeks after limit runes and returns false.
|
// If limit is non-zero, seekPosition stops seeks after limit runes and returns false.
|
||||||
func seekPosition(lines []text.Line, start, needle combinedPos, alignment text.Alignment, width, limit int) (combinedPos, bool) {
|
func seekPosition(lines []text.Line, alignment text.Alignment, width int, start, needle combinedPos, limit int) (combinedPos, bool) {
|
||||||
l := lines[start.lineCol.Y]
|
l := lines[start.lineCol.Y]
|
||||||
count := 0
|
count := 0
|
||||||
// Advance next and prev until next is greater than or equal to pos.
|
// Advance next and prev until next is greater than or equal to pos.
|
||||||
|
|||||||
+65
-131
@@ -30,125 +30,57 @@ type Label struct {
|
|||||||
// not pixels): Y = line number, X = rune column.
|
// not pixels): Y = line number, X = rune column.
|
||||||
type screenPos image.Point
|
type screenPos image.Point
|
||||||
|
|
||||||
type segmentIterator struct {
|
|
||||||
Lines []text.Line
|
|
||||||
Clip image.Rectangle
|
|
||||||
Alignment text.Alignment
|
|
||||||
Width int
|
|
||||||
Offset image.Point
|
|
||||||
startSel screenPos
|
|
||||||
endSel screenPos
|
|
||||||
|
|
||||||
pos screenPos // current position
|
|
||||||
line text.Line // current line
|
|
||||||
layout text.Layout // current line's Layout
|
|
||||||
|
|
||||||
// pixel positions
|
|
||||||
off fixed.Point26_6
|
|
||||||
y, prevDesc fixed.Int26_6
|
|
||||||
}
|
|
||||||
|
|
||||||
const inf = 1e6
|
const inf = 1e6
|
||||||
|
|
||||||
func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, image.Point, bool) {
|
func posIsAbove(lines []text.Line, pos combinedPos, y int) bool {
|
||||||
for l.pos.Y < len(l.Lines) {
|
line := lines[pos.lineCol.Y]
|
||||||
if l.pos.X == 0 {
|
return pos.y+line.Bounds.Max.Y.Ceil() < y
|
||||||
l.line = l.Lines[l.pos.Y]
|
|
||||||
|
|
||||||
// Calculate X & Y pixel coordinates of left edge of line. We need y
|
|
||||||
// for the next line, so it's in l, but we only need x here, so it's
|
|
||||||
// not.
|
|
||||||
x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X)
|
|
||||||
l.y += l.prevDesc + l.line.Ascent
|
|
||||||
l.prevDesc = l.line.Descent
|
|
||||||
// Align baseline and line start to the pixel grid.
|
|
||||||
l.off = fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())}
|
|
||||||
l.y = l.off.Y
|
|
||||||
l.off.Y += fixed.I(l.Offset.Y)
|
|
||||||
if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
|
|
||||||
// This line is outside/before the clip area; go on to the next line.
|
|
||||||
l.pos.Y++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the line's Layout, since we slice it up later.
|
|
||||||
l.layout = l.line.Layout
|
|
||||||
|
|
||||||
// Find the left edge of the text visible in the l.Clip clipping
|
|
||||||
// area.
|
|
||||||
for len(l.layout.Advances) > 0 {
|
|
||||||
_, n := utf8.DecodeRuneInString(l.layout.Text)
|
|
||||||
adv := l.layout.Advances[0]
|
|
||||||
if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
l.off.X += adv
|
|
||||||
l.layout.Text = l.layout.Text[n:]
|
|
||||||
l.layout.Advances = l.layout.Advances[1:]
|
|
||||||
l.pos.X++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selected := l.inSelection()
|
|
||||||
endx := l.off.X
|
|
||||||
rune := 0
|
|
||||||
nextLine := true
|
|
||||||
retLayout := l.layout
|
|
||||||
for n := range l.layout.Text {
|
|
||||||
selChanged := selected != l.inSelection()
|
|
||||||
beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X
|
|
||||||
if selChanged || beyondClipEdge {
|
|
||||||
retLayout.Advances = l.layout.Advances[:rune]
|
|
||||||
retLayout.Text = l.layout.Text[:n]
|
|
||||||
if selChanged {
|
|
||||||
// Save the rest of the line
|
|
||||||
l.layout.Advances = l.layout.Advances[rune:]
|
|
||||||
l.layout.Text = l.layout.Text[n:]
|
|
||||||
nextLine = false
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
endx += l.layout.Advances[rune]
|
|
||||||
rune++
|
|
||||||
l.pos.X++
|
|
||||||
}
|
|
||||||
offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()}
|
|
||||||
|
|
||||||
// Calculate the width & height if the returned text.
|
|
||||||
//
|
|
||||||
// If there's a better way to do this, I'm all ears.
|
|
||||||
var d fixed.Int26_6
|
|
||||||
for _, adv := range retLayout.Advances {
|
|
||||||
d += adv
|
|
||||||
}
|
|
||||||
size := image.Point{
|
|
||||||
X: d.Ceil(),
|
|
||||||
Y: (l.line.Ascent + l.line.Descent).Ceil(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextLine {
|
|
||||||
l.pos.Y++
|
|
||||||
l.pos.X = 0
|
|
||||||
} else {
|
|
||||||
l.off.X = endx
|
|
||||||
}
|
|
||||||
|
|
||||||
return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true
|
|
||||||
}
|
|
||||||
return text.Layout{}, image.Point{}, false, 0, image.Point{}, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *segmentIterator) inSelection() bool {
|
func posIsBelow(lines []text.Line, pos combinedPos, y int) bool {
|
||||||
return l.startSel.LessOrEqual(l.pos) &&
|
line := lines[pos.lineCol.Y]
|
||||||
l.pos.Less(l.endSel)
|
return pos.y+line.Bounds.Min.Y.Floor() > y
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p1 screenPos) LessOrEqual(p2 screenPos) bool {
|
func clipLine(lines []text.Line, alignment text.Alignment, width int, clip image.Rectangle, linePos combinedPos) (start combinedPos, end combinedPos) {
|
||||||
return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X)
|
// Seek to first (potentially) visible column.
|
||||||
|
lineIdx := linePos.lineCol.Y
|
||||||
|
line := lines[lineIdx]
|
||||||
|
// runeWidth is the width of the widest rune in line.
|
||||||
|
runeWidth := (line.Bounds.Max.X - line.Width).Ceil()
|
||||||
|
q := combinedPos{y: start.y, x: fixed.I(clip.Min.X - runeWidth)}
|
||||||
|
start, _ = seekPosition(lines, alignment, width, linePos, q, 0)
|
||||||
|
// Seek to first invisible column after start.
|
||||||
|
q = combinedPos{y: start.y, x: fixed.I(clip.Max.X + runeWidth)}
|
||||||
|
end, _ = seekPosition(lines, alignment, width, start, q, 0)
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
|
|
||||||
|
func subLayout(line text.Line, startCol, endCol int) text.Layout {
|
||||||
|
adv := line.Layout.Advances
|
||||||
|
if startCol == len(adv) {
|
||||||
|
return text.Layout{}
|
||||||
|
}
|
||||||
|
adv = adv[startCol:endCol]
|
||||||
|
txt := line.Layout.Text
|
||||||
|
for i := 0; i < startCol; i++ {
|
||||||
|
_, s := utf8.DecodeRuneInString(txt)
|
||||||
|
txt = txt[s:]
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for i := startCol; i < endCol; i++ {
|
||||||
|
_, s := utf8.DecodeRuneInString(txt[n:])
|
||||||
|
n += s
|
||||||
|
}
|
||||||
|
txt = txt[:n]
|
||||||
|
return text.Layout{Text: txt, Advances: adv}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstPos(line text.Line, alignment text.Alignment, width int) combinedPos {
|
||||||
|
return combinedPos{
|
||||||
|
x: align(alignment, line.Width, width),
|
||||||
|
y: line.Ascent.Ceil(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p1 screenPos) Less(p2 screenPos) bool {
|
func (p1 screenPos) Less(p2 screenPos) bool {
|
||||||
@@ -164,29 +96,31 @@ func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size un
|
|||||||
}
|
}
|
||||||
dims := linesDimens(lines)
|
dims := linesDimens(lines)
|
||||||
dims.Size = cs.Constrain(dims.Size)
|
dims.Size = cs.Constrain(dims.Size)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return dims
|
||||||
|
}
|
||||||
cl := textPadding(lines)
|
cl := textPadding(lines)
|
||||||
cl.Max = cl.Max.Add(dims.Size)
|
cl.Max = cl.Max.Add(dims.Size)
|
||||||
it := segmentIterator{
|
defer clip.Rect(cl).Push(gtx.Ops).Pop()
|
||||||
Lines: lines,
|
semantic.LabelOp(txt).Add(gtx.Ops)
|
||||||
Clip: cl,
|
pos := firstPos(lines[0], l.Alignment, dims.Size.X)
|
||||||
Alignment: l.Alignment,
|
for !posIsBelow(lines, pos, cl.Max.Y) {
|
||||||
Width: dims.Size.X,
|
start, end := clipLine(lines, l.Alignment, dims.Size.X, cl, pos)
|
||||||
}
|
line := lines[start.lineCol.Y]
|
||||||
for {
|
lt := subLayout(line, start.lineCol.X, end.lineCol.X)
|
||||||
l, off, _, _, _, ok := it.Next()
|
|
||||||
if !ok {
|
off := image.Point{X: start.x.Floor(), Y: start.y}
|
||||||
|
t := op.Offset(layout.FPt(off)).Push(gtx.Ops)
|
||||||
|
op := clip.Outline{Path: s.Shape(font, textSize, lt)}.Op().Push(gtx.Ops)
|
||||||
|
paint.PaintOp{}.Add(gtx.Ops)
|
||||||
|
op.Pop()
|
||||||
|
t.Pop()
|
||||||
|
|
||||||
|
if pos.lineCol.Y == len(lines)-1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
t := op.Offset(layout.FPt(off)).Push(gtx.Ops)
|
pos, _ = seekPosition(lines, l.Alignment, dims.Size.X, pos, combinedPos{lineCol: screenPos{Y: pos.lineCol.Y + 1}}, 0)
|
||||||
rcl := clip.Rect(cl.Sub(off)).Push(gtx.Ops)
|
|
||||||
cl := clip.Outline{Path: s.Shape(font, textSize, l)}.Op().Push(gtx.Ops)
|
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
|
||||||
cl.Pop()
|
|
||||||
rcl.Pop()
|
|
||||||
t.Pop()
|
|
||||||
}
|
}
|
||||||
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
|
|
||||||
semantic.LabelOp(txt).Add(gtx.Ops)
|
|
||||||
return dims
|
return dims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user