Overhauled event system
This commit is contained in:
		
							parent
							
								
									3a3fb66db8
								
							
						
					
					
						commit
						e030f8632b
					
				| @ -10,12 +10,12 @@ type Application struct { | ||||
| 	icons []image.Image | ||||
| 	backend Backend | ||||
| 	config Config | ||||
| 	callbackManager CallbackManager | ||||
| } | ||||
| 
 | ||||
| // Run initializes the application, starts it, and then returns a channel that | ||||
| // broadcasts events. If no suitable backend can be found, an error is returned. | ||||
| func (application *Application) Run () ( | ||||
| 	channel chan(Event), | ||||
| 	err     error, | ||||
| ) { | ||||
| 	// default values for certain parameters | ||||
| @ -29,12 +29,46 @@ func (application *Application) Run () ( | ||||
| 	application.backend, err = instantiateBackend(application) | ||||
| 	if err != nil { return } | ||||
| 	 | ||||
| 	channel = make(chan(Event)) | ||||
| 	go application.backend.Run(channel) | ||||
| 
 | ||||
| 	application.backend.Run() | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (application *Application) OnQuit ( | ||||
| 	onQuit func (), | ||||
| ) { | ||||
| 	application.callbackManager.onQuit = onQuit | ||||
| } | ||||
| 
 | ||||
| func (application *Application) OnPress ( | ||||
| 	onPress func (button Button), | ||||
| ) { | ||||
| 	application.callbackManager.onPress = onPress | ||||
| } | ||||
| 
 | ||||
| func (application *Application) OnRelease ( | ||||
| 	onRelease func (button Button), | ||||
| ) { | ||||
| 	application.callbackManager.onRelease = onRelease | ||||
| } | ||||
| 
 | ||||
| func (application *Application) OnResize ( | ||||
| 	onResize func (), | ||||
| ) { | ||||
| 	application.callbackManager.onResize = onResize | ||||
| } | ||||
| 
 | ||||
| func (application *Application) OnMouseMove ( | ||||
| 	onMouseMove func (x, y int), | ||||
| ) { | ||||
| 	application.callbackManager.onMouseMove = onMouseMove | ||||
| } | ||||
| 
 | ||||
| func (application *Application) OnStart ( | ||||
| 	onStart func (), | ||||
| ) { | ||||
| 	application.callbackManager.onStart = onStart | ||||
| } | ||||
| 
 | ||||
| // Draw "commits" changes made in the buffer to the display. | ||||
| func (application *Application) Draw () { | ||||
| 	application.backend.Draw() | ||||
|  | ||||
							
								
								
									
										12
									
								
								backend.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								backend.go
									
									
									
									
									
								
							| @ -4,13 +4,19 @@ import "image" | ||||
| import "errors" | ||||
| 
 | ||||
| type Backend interface { | ||||
| 	Run      (channel chan(Event)) | ||||
| 	Run      () | ||||
| 	SetTitle (title string) (err error) | ||||
| 	SetIcon  (icons []image.Image) (err error) | ||||
| 	Draw     () | ||||
| } | ||||
| 
 | ||||
| type BackendFactory func (application *Application) (backend Backend, err error) | ||||
| type BackendFactory func ( | ||||
| 	application *Application, | ||||
| 	callbackManager *CallbackManager, | ||||
| ) ( | ||||
| 	backend Backend, | ||||
| 	err error, | ||||
| ) | ||||
| 
 | ||||
| var factories []BackendFactory | ||||
| 
 | ||||
| @ -21,7 +27,7 @@ func RegisterBackend (factory BackendFactory) { | ||||
| func instantiateBackend (application *Application) (backend Backend, err error) { | ||||
| 	// find a suitable backend | ||||
| 	for _, factory := range factories { | ||||
| 		backend, err = factory(application) | ||||
| 		backend, err = factory(application, &application.callbackManager) | ||||
| 		if err == nil && backend != nil { return } | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -3,12 +3,13 @@ package x | ||||
| import "image" | ||||
| import "image/draw" | ||||
| import "golang.org/x/image/math/fixed" | ||||
| import "github.com/jezek/xgbutil/xgraphics" | ||||
| 
 | ||||
| import "git.tebibyte.media/sashakoshka/stone" | ||||
| 
 | ||||
| func (backend *Backend) Draw () { | ||||
| 	backend.drawLock.Lock() | ||||
| 	defer backend.drawLock.Unlock() | ||||
| 	backend.lock.Lock() | ||||
| 	defer backend.lock.Unlock() | ||||
| 
 | ||||
| 	boundsChanged := | ||||
| 		backend.memory.windowWidth  != backend.metrics.windowWidth || | ||||
| @ -22,25 +23,77 @@ func (backend *Backend) Draw () { | ||||
| 		backend.canvas.XDraw() | ||||
| 		backend.canvas.XPaint(backend.window.Id) | ||||
| 	} else { | ||||
| 		backend.updateWindowAreas(backend.drawCells(false)...) | ||||
| 		backend.canvas.XPaintRects ( | ||||
| 			backend.window.Id, | ||||
| 			backend.drawCells(false)...) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) updateWindowAreas (areas ...image.Rectangle) { | ||||
| 	backend.canvas.XPaintRects(backend.window.Id, areas...) | ||||
| func (backend *Backend) reallocateCanvas () { | ||||
| 	if backend.canvas != nil { | ||||
| 		backend.canvas.Destroy() | ||||
| 	} | ||||
| 	backend.canvas = xgraphics.New ( | ||||
| 		backend.connection, | ||||
| 		image.Rect ( | ||||
| 			0, 0, | ||||
| 			backend.metrics.windowWidth, | ||||
| 			backend.metrics.windowHeight)) | ||||
| 	backend.canvas.For (func (x, y int) xgraphics.BGRA { | ||||
| 		return backend.colors[stone.ColorBackground] | ||||
| 	}) | ||||
| 	 | ||||
| 	backend.canvas.XSurfaceSet(backend.window.Id) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) drawRune (x, y int, character rune, runeColor stone.Color) { | ||||
| func (backend *Backend) drawCells (forceRedraw bool) (areas []image.Rectangle) { | ||||
| 	width, height := backend.application.Size() | ||||
| 	for y := 0; y < height; y ++ { | ||||
| 	for x := 0; x < width;  x ++ { | ||||
| 		if !forceRedraw && backend.application.Clean(x, y) { continue } | ||||
| 		backend.application.MarkClean(x, y) | ||||
| 
 | ||||
| 		cell := backend.application.Cell(x, y) | ||||
| 		content := cell.Rune() | ||||
| 
 | ||||
| 		if forceRedraw && content < 32 { continue } | ||||
| 
 | ||||
| 		areas = append(areas, backend.boundsOfCell(x, y)) | ||||
| 		backend.drawRune(x, y, content, cell.Color(), !forceRedraw) | ||||
| 	}} | ||||
| 
 | ||||
| 	if backend.drawBufferBounds && forceRedraw { | ||||
| 		strokeRectangle ( | ||||
| 			&image.Uniform { | ||||
| 				C: backend.config.Color(stone.ColorForeground), | ||||
| 			}, | ||||
| 			backend.canvas, | ||||
| 			image.Rectangle { | ||||
| 				Min: backend.originOfCell(0, 0), | ||||
| 				Max: backend.originOfCell(width, height), | ||||
| 			}) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) drawRune ( | ||||
| 	x, y int, | ||||
| 	character rune, | ||||
| 	runeColor stone.Color, | ||||
| 	drawBackground bool, | ||||
| ) { | ||||
| 	// TODO: cache these draws as non-transparent buffers with the | ||||
| 	// application background color as the background. that way, we won't | ||||
| 	// need to redraw the characters *or* composite them. | ||||
| 
 | ||||
| 	if drawBackground { | ||||
| 		fillRectangle ( | ||||
| 			&image.Uniform { | ||||
| 				C: backend.config.Color(stone.ColorBackground), | ||||
| 			}, | ||||
| 			backend.canvas, | ||||
| 			backend.boundsOfCell(x, y)) | ||||
| 	} | ||||
| 
 | ||||
| 	if character < 32 { return } | ||||
| 	 | ||||
| @ -73,36 +126,6 @@ func (backend *Backend) drawRune (x, y int, character rune, runeColor stone.Colo | ||||
| 		draw.Over) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) drawCells (forceRedraw bool) (areas []image.Rectangle) { | ||||
| 	width, height := backend.application.Size() | ||||
| 	for y := 0; y < height; y ++ { | ||||
| 	for x := 0; x < width;  x ++ { | ||||
| 		if !forceRedraw && backend.application.Clean(x, y) { continue } | ||||
| 		backend.application.MarkClean(x, y) | ||||
| 
 | ||||
| 		cell := backend.application.Cell(x, y) | ||||
| 		content := cell.Rune() | ||||
| 
 | ||||
| 		if forceRedraw && content < 32 { continue } | ||||
| 
 | ||||
| 		areas = append(areas, backend.boundsOfCell(x, y)) | ||||
| 		backend.drawRune(x, y, content, cell.Color()) | ||||
| 	}} | ||||
| 
 | ||||
| 	if backend.drawBufferBounds && forceRedraw { | ||||
| 		strokeRectangle ( | ||||
| 			&image.Uniform { | ||||
| 				C: backend.config.Color(stone.ColorForeground), | ||||
| 			}, | ||||
| 			backend.canvas, | ||||
| 			image.Rectangle { | ||||
| 				Min: backend.originOfCell(0, 0), | ||||
| 				Max: backend.originOfCell(width, height), | ||||
| 			}) | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func fillRectangle ( | ||||
| 	source      image.Image, | ||||
| 	destination draw.Image, | ||||
|  | ||||
| @ -8,17 +8,19 @@ import "github.com/jezek/xgbutil/xevent" | ||||
| 
 | ||||
| import "git.tebibyte.media/sashakoshka/stone" | ||||
| 
 | ||||
| func (backend *Backend) Run (channel chan(stone.Event)) { | ||||
| 	backend.channel = channel | ||||
| func (backend *Backend) Run () { | ||||
| 	backend.callbackManager.RunStart() | ||||
| 	backend.Draw() | ||||
| 	xevent.Main(backend.connection) | ||||
| 	backend.shutDown() | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| func (backend *Backend) handleConfigureNotify ( | ||||
| 	connection *xgbutil.XUtil, | ||||
| 	event xevent.ConfigureNotifyEvent, | ||||
| ) { | ||||
| 	backend.lock.Lock() | ||||
| 	 | ||||
| 	configureEvent := *event.ConfigureNotifyEvent | ||||
| 	 | ||||
| 	newWidth  := int(configureEvent.Width) | ||||
| @ -44,8 +46,13 @@ func (backend *Backend) handleConfigureNotify ( | ||||
| 			(backend.metrics.windowWidth - frameWidth) / 2 | ||||
| 		backend.metrics.paddingY = | ||||
| 			(backend.metrics.windowHeight - frameHeight) / 2 | ||||
| 	} | ||||
| 	 | ||||
| 		backend.channel <- stone.EventResize { } | ||||
| 	backend.lock.Unlock() | ||||
| 
 | ||||
| 	if sizeChanged { | ||||
| 		backend.callbackManager.RunResize() | ||||
| 		backend.Draw() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -54,9 +61,7 @@ func (backend *Backend) handleButtonPress ( | ||||
| 	event xevent.ButtonPressEvent, | ||||
| ) { | ||||
| 	buttonEvent := *event.ButtonPressEvent | ||||
| 	backend.channel <- stone.EventPress { | ||||
| 		Button: stone.Button(buttonEvent.Detail + 127), | ||||
| 	} | ||||
| 	backend.callbackManager.RunPress(stone.Button(buttonEvent.Detail + 127)) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) handleButtonRelease ( | ||||
| @ -64,9 +69,7 @@ func (backend *Backend) handleButtonRelease ( | ||||
| 	event xevent.ButtonReleaseEvent, | ||||
| ) { | ||||
| 	buttonEvent := *event.ButtonReleaseEvent | ||||
| 	backend.channel <- stone.EventRelease { | ||||
| 		Button: stone.Button(buttonEvent.Detail + 127), | ||||
| 	} | ||||
| 	backend.callbackManager.RunRelease(stone.Button(buttonEvent.Detail + 127)) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) handleKeyPress ( | ||||
| @ -75,7 +78,7 @@ func (backend *Backend) handleKeyPress ( | ||||
| ) { | ||||
| 	keyEvent := *event.KeyPressEvent | ||||
| 	button   := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) | ||||
| 	backend.channel <- stone.EventPress { Button: button } | ||||
| 	backend.callbackManager.RunPress(button) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) handleKeyRelease ( | ||||
| @ -84,7 +87,7 @@ func (backend *Backend) handleKeyRelease ( | ||||
| ) { | ||||
| 	keyEvent := *event.KeyReleaseEvent | ||||
| 	button   := backend.keycodeToButton(keyEvent.Detail, keyEvent.State) | ||||
| 	backend.channel <- stone.EventRelease { Button: button } | ||||
| 	backend.callbackManager.RunRelease(button) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) handleMotionNotify ( | ||||
| @ -96,10 +99,7 @@ func (backend *Backend) handleMotionNotify ( | ||||
| 		X: int(motionEvent.EventX), | ||||
| 		Y: int(motionEvent.EventY), | ||||
| 	}) | ||||
| 	backend.channel <- stone.EventMouseMove { | ||||
| 		X: x, | ||||
| 		Y: y, | ||||
| 	} | ||||
| 	backend.callbackManager.RunMouseMove(x, y) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) compressConfigureNotify ( | ||||
| @ -127,5 +127,5 @@ func (backend *Backend) compressConfigureNotify ( | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) shutDown () { | ||||
| 	backend.channel <- stone.EventQuit { } | ||||
| 	backend.callbackManager.RunQuit() | ||||
| } | ||||
|  | ||||
| @ -18,10 +18,17 @@ import "git.tebibyte.media/sashakoshka/stone" | ||||
| import "github.com/flopp/go-findfont" | ||||
| 
 | ||||
| // factory instantiates an X backend. | ||||
| func factory (application *stone.Application) (output stone.Backend, err error) { | ||||
| func factory ( | ||||
| 	application *stone.Application, | ||||
| 	callbackManager *stone.CallbackManager, | ||||
| ) ( | ||||
| 	output stone.Backend, | ||||
| 	err error, | ||||
| ) { | ||||
| 	backend := &Backend { | ||||
| 		application:     application, | ||||
| 		config:          application.Config(), | ||||
| 		callbackManager: callbackManager, | ||||
| 	} | ||||
| 
 | ||||
| 	// load font | ||||
| @ -76,8 +83,6 @@ func factory (application *stone.Application) (output stone.Backend, err error) | ||||
| 		backend.metrics.windowWidth, backend.metrics.windowHeight, | ||||
| 		0) | ||||
| 	backend.window.Map() | ||||
| 	// TODO: also listen to mouse movement (compressed) and mouse and | ||||
| 	// keyboard buttons (uncompressed) | ||||
| 	err = backend.window.Listen ( | ||||
| 		xproto.EventMaskStructureNotify, | ||||
| 		xproto.EventMaskPointerMotion, | ||||
|  | ||||
| @ -16,15 +16,15 @@ import "git.tebibyte.media/sashakoshka/stone" | ||||
| type Backend struct { | ||||
| 	application     *stone.Application | ||||
| 	config          *stone.Config | ||||
| 	callbackManager *stone.CallbackManager | ||||
| 	connection      *xgbutil.XUtil | ||||
| 	window          *xwindow.Window | ||||
| 	canvas          *xgraphics.Image | ||||
| 	channel     chan(stone.Event) | ||||
| 
 | ||||
| 	drawCellBounds   bool | ||||
| 	drawBufferBounds bool | ||||
| 
 | ||||
| 	drawLock sync.Mutex | ||||
| 	lock sync.Mutex | ||||
| 
 | ||||
| 	font struct { | ||||
| 		face font.Face | ||||
| @ -114,23 +114,6 @@ func (backend *Backend) calculateBufferSize () (width, height int) { | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) reallocateCanvas () { | ||||
| 	if backend.canvas != nil { | ||||
| 		backend.canvas.Destroy() | ||||
| 	} | ||||
| 	backend.canvas = xgraphics.New ( | ||||
| 		backend.connection, | ||||
| 		image.Rect ( | ||||
| 			0, 0, | ||||
| 			backend.metrics.windowWidth, | ||||
| 			backend.metrics.windowHeight)) | ||||
| 	backend.canvas.For (func (x, y int) xgraphics.BGRA { | ||||
| 		return backend.colors[stone.ColorBackground] | ||||
| 	}) | ||||
| 	 | ||||
| 	backend.canvas.XSurfaceSet(backend.window.Id) | ||||
| } | ||||
| 
 | ||||
| func (backend *Backend) cellAt (onScreen image.Point) (x, y int) { | ||||
| 	x = (onScreen.X - backend.metrics.paddingX) / backend.metrics.cellWidth | ||||
| 	y = (onScreen.Y - backend.metrics.paddingY) / backend.metrics.cellHeight | ||||
|  | ||||
| @ -153,6 +153,10 @@ func (buffer *Buffer) SetRune (x, y int, content rune) { | ||||
| 	buffer.lock.RLock() | ||||
| 	defer buffer.lock.RUnlock() | ||||
| 	 | ||||
| 	buffer.setRune(x, y, content) | ||||
| } | ||||
| 
 | ||||
| func (buffer *Buffer) setRune (x, y int, content rune) { | ||||
| 	if buffer.isOutOfBounds(x, y) { return } | ||||
| 	index := x + y * buffer.width | ||||
| 	buffer.clean[index] = buffer.content[index].content == content | ||||
| @ -169,7 +173,7 @@ func (buffer *Buffer) Write (bytes []byte) (bytesWritten int, err error) { | ||||
| 	bytesWritten = len(bytes) | ||||
| 	 | ||||
| 	for _, character := range text { | ||||
| 		buffer.SetRune(buffer.dot.x, buffer.dot.y, character) | ||||
| 		buffer.setRune(buffer.dot.x, buffer.dot.y, character) | ||||
| 		buffer.dot.x ++ | ||||
| 		if buffer.dot.x > buffer.width { break } | ||||
| 	} | ||||
|  | ||||
							
								
								
									
										61
									
								
								event.go
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								event.go
									
									
									
									
									
								
							| @ -1,27 +1,40 @@ | ||||
| package stone | ||||
| 
 | ||||
| // Event can be any event. | ||||
| type Event interface { } | ||||
| 
 | ||||
| // EventQuit is sent when the backend shuts down due to a window close, error, | ||||
| // or something else. | ||||
| type EventQuit struct { } | ||||
| 
 | ||||
| // EventPress is sent when a button is pressed, or a key repeat event is | ||||
| // triggered. | ||||
| type EventPress struct { Button } | ||||
| 
 | ||||
| // Release is sent when a button is released. | ||||
| type EventRelease struct { Button } | ||||
| 
 | ||||
| // Resize is sent when the application window is resized by the user. This event | ||||
| // must be handled, as it implies that the buffer has been resized and therefore | ||||
| // cleared. Application.Draw() must be called after this event is recieved. | ||||
| type EventResize struct { } | ||||
| 
 | ||||
| // EventMouseMove is sent when the mouse changes position. It contains the X and | ||||
| // Y position of the mouse. | ||||
| type EventMouseMove struct { | ||||
| 	X int | ||||
| 	Y int | ||||
| type CallbackManager struct { | ||||
| 	onQuit      func () | ||||
| 	onPress     func (button Button) | ||||
| 	onRelease   func (button Button) | ||||
| 	onResize    func () | ||||
| 	onMouseMove func (x, y int) | ||||
| 	onStart     func () | ||||
| } | ||||
| 
 | ||||
| func (manager *CallbackManager) RunQuit () { | ||||
| 	if manager.onQuit == nil { return } | ||||
| 	manager.onQuit() | ||||
| } | ||||
| 
 | ||||
| func (manager *CallbackManager) RunPress (button Button) { | ||||
| 	if manager.onPress == nil { return } | ||||
| 	manager.onPress(button) | ||||
| } | ||||
| 
 | ||||
| func (manager *CallbackManager) RunRelease (button Button) { | ||||
| 	if manager.onRelease == nil { return } | ||||
| 	manager.onRelease(button) | ||||
| } | ||||
| 
 | ||||
| func (manager *CallbackManager) RunResize () { | ||||
| 	if manager.onResize == nil { return } | ||||
| 	manager.onResize() | ||||
| } | ||||
| 
 | ||||
| func (manager *CallbackManager) RunMouseMove (x, y int) { | ||||
| 	if manager.onMouseMove == nil { return } | ||||
| 	manager.onMouseMove(x, y) | ||||
| } | ||||
| 
 | ||||
| func (manager *CallbackManager) RunStart () { | ||||
| 	if manager.onStart == nil { return } | ||||
| 	manager.onStart() | ||||
| } | ||||
|  | ||||
| @ -10,7 +10,7 @@ var application = &stone.Application { } | ||||
| var mousePressed bool | ||||
| 
 | ||||
| func main () { | ||||
| 	application.SetTitle("hellorld") | ||||
| 	application.SetTitle("drawing canvas") | ||||
| 	application.SetSize(32, 16) | ||||
| 
 | ||||
| 	iconFile16, err := os.Open("assets/scaffold16.png") | ||||
| @ -26,42 +26,37 @@ func main () { | ||||
| 	 | ||||
| 	application.SetIcon([]image.Image { icon16, icon32 }) | ||||
| 	 | ||||
| 	channel, err := application.Run() | ||||
| 	application.OnPress(onPress) | ||||
| 	application.OnRelease(onRelease) | ||||
| 	application.OnMouseMove(onMouseMove) | ||||
| 	application.OnQuit(onQuit) | ||||
| 	 | ||||
| 	err = application.Run() | ||||
| 	if err != nil { panic(err) } | ||||
| } | ||||
| 
 | ||||
| 	application.Draw() | ||||
| 
 | ||||
| 	for { | ||||
| 		event := <- channel | ||||
| 		switch event.(type) { | ||||
| 		case stone.EventQuit: | ||||
| 			os.Exit(0) | ||||
| 
 | ||||
| 		case stone.EventPress: | ||||
| 			button := event.(stone.EventPress).Button | ||||
| func onPress (button stone.Button) { | ||||
| 	if button == stone.MouseButtonLeft { | ||||
| 		mousePressed = true | ||||
| 		application.SetRune(0, 0, '+') | ||||
| 		application.Draw() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 		case stone.EventRelease: | ||||
| 			button := event.(stone.EventRelease).Button | ||||
| func onRelease (button stone.Button) { | ||||
| 	if button == stone.MouseButtonLeft { | ||||
| 		mousePressed = false | ||||
| 		application.SetRune(0, 0, 0) | ||||
| 		application.Draw() | ||||
| 	} | ||||
| 
 | ||||
| 		case stone.EventMouseMove: | ||||
| 			event := event.(stone.EventMouseMove) | ||||
| 			if mousePressed { | ||||
| 				application.SetRune(event.X, event.Y, '#') | ||||
| 				application.Draw() | ||||
| } | ||||
| 
 | ||||
| 		case stone.EventResize: | ||||
| func onMouseMove (x, y int) {	if mousePressed { | ||||
| 		application.SetRune(x, y, '#') | ||||
| 		application.Draw() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func onQuit () { | ||||
| 	os.Exit(0) | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user