mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
remove render timer from window
This commit is contained in:
@ -25,8 +25,6 @@ const Config = @import("config.zig").Config;
|
|||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const DevMode = @import("DevMode.zig");
|
const DevMode = @import("DevMode.zig");
|
||||||
|
|
||||||
const RenderTimer = max_timer.MaxTimer(renderTimerCallback);
|
|
||||||
|
|
||||||
const log = std.log.scoped(.window);
|
const log = std.log.scoped(.window);
|
||||||
|
|
||||||
// The preallocation size for the write request pool. This should be big
|
// The preallocation size for the write request pool. This should be big
|
||||||
@ -83,9 +81,6 @@ terminal_stream: terminal.Stream(*Window),
|
|||||||
/// Cursor state.
|
/// Cursor state.
|
||||||
terminal_cursor: Cursor,
|
terminal_cursor: Cursor,
|
||||||
|
|
||||||
/// Render at least 60fps.
|
|
||||||
render_timer: RenderTimer,
|
|
||||||
|
|
||||||
/// The dimensions of the grid in rows and columns.
|
/// The dimensions of the grid in rows and columns.
|
||||||
grid_size: renderer.GridSize,
|
grid_size: renderer.GridSize,
|
||||||
|
|
||||||
@ -500,7 +495,6 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
|||||||
.terminal_stream = .{ .handler = self },
|
.terminal_stream = .{ .handler = self },
|
||||||
.terminal_cursor = .{ .timer = timer },
|
.terminal_cursor = .{ .timer = timer },
|
||||||
.grid_size = grid_size,
|
.grid_size = grid_size,
|
||||||
.render_timer = try RenderTimer.init(loop, self, 6, 12),
|
|
||||||
.pty_stream = stream,
|
.pty_stream = stream,
|
||||||
.config = config,
|
.config = config,
|
||||||
.bg_r = @intToFloat(f32, config.background.r) / 255.0,
|
.bg_r = @intToFloat(f32, config.background.r) / 255.0,
|
||||||
@ -616,8 +610,6 @@ pub fn destroy(self: *Window) void {
|
|||||||
}
|
}
|
||||||
}).callback);
|
}).callback);
|
||||||
|
|
||||||
self.render_timer.deinit();
|
|
||||||
|
|
||||||
// We have to dealloc our window in the close callback because
|
// We have to dealloc our window in the close callback because
|
||||||
// we can't free some of the memory associated with the window
|
// we can't free some of the memory associated with the window
|
||||||
// until the stream is closed.
|
// until the stream is closed.
|
||||||
@ -668,6 +660,13 @@ fn queueWrite(self: *Window, data: []const u8) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This queues a render operation with the renderer thread. The render
|
||||||
|
/// isn't guaranteed to happen immediately but it will happen as soon as
|
||||||
|
/// practical.
|
||||||
|
fn queueRender(self: *const Window) !void {
|
||||||
|
try self.renderer_thread.wakeup.send();
|
||||||
|
}
|
||||||
|
|
||||||
/// The cursor position from glfw directly is in screen coordinates but
|
/// The cursor position from glfw directly is in screen coordinates but
|
||||||
/// all our internal state works in pixels.
|
/// all our internal state works in pixels.
|
||||||
fn cursorPosToPixels(self: Window, pos: glfw.Window.CursorPos) glfw.Window.CursorPos {
|
fn cursorPosToPixels(self: Window, pos: glfw.Window.CursorPos) glfw.Window.CursorPos {
|
||||||
@ -719,7 +718,7 @@ fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
|
|||||||
const win = window.getUserPointer(Window) orelse return;
|
const win = window.getUserPointer(Window) orelse return;
|
||||||
|
|
||||||
// Resize usually forces a redraw
|
// Resize usually forces a redraw
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render timer in sizeCallback err={}", .{err});
|
log.err("error scheduling render timer in sizeCallback err={}", .{err});
|
||||||
|
|
||||||
// Recalculate our grid size
|
// Recalculate our grid size
|
||||||
@ -758,7 +757,7 @@ fn charCallback(window: glfw.Window, codepoint: u21) void {
|
|||||||
// If the event was handled by imgui, ignore it.
|
// If the event was handled by imgui, ignore it.
|
||||||
if (imgui.IO.get()) |io| {
|
if (imgui.IO.get()) |io| {
|
||||||
if (io.cval().WantCaptureKeyboard) {
|
if (io.cval().WantCaptureKeyboard) {
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render timer err={}", .{err});
|
log.err("error scheduling render timer err={}", .{err});
|
||||||
}
|
}
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
@ -773,7 +772,7 @@ fn charCallback(window: glfw.Window, codepoint: u21) void {
|
|||||||
// Anytime is character is created, we have to clear the selection
|
// Anytime is character is created, we have to clear the selection
|
||||||
if (win.terminal.selection != null) {
|
if (win.terminal.selection != null) {
|
||||||
win.terminal.selection = null;
|
win.terminal.selection = null;
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render in charCallback err={}", .{err});
|
log.err("error scheduling render in charCallback err={}", .{err});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -781,7 +780,7 @@ fn charCallback(window: glfw.Window, codepoint: u21) void {
|
|||||||
// TODO: detect if we're at the bottom to avoid the render call here.
|
// TODO: detect if we're at the bottom to avoid the render call here.
|
||||||
win.terminal.scrollViewport(.{ .bottom = {} }) catch |err|
|
win.terminal.scrollViewport(.{ .bottom = {} }) catch |err|
|
||||||
log.err("error scrolling viewport err={}", .{err});
|
log.err("error scrolling viewport err={}", .{err});
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render in charCallback err={}", .{err});
|
log.err("error scheduling render in charCallback err={}", .{err});
|
||||||
|
|
||||||
// Write the character to the pty
|
// Write the character to the pty
|
||||||
@ -806,7 +805,7 @@ fn keyCallback(
|
|||||||
// If the event was handled by imgui, ignore it.
|
// If the event was handled by imgui, ignore it.
|
||||||
if (imgui.IO.get()) |io| {
|
if (imgui.IO.get()) |io| {
|
||||||
if (io.cval().WantCaptureKeyboard) {
|
if (io.cval().WantCaptureKeyboard) {
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render timer err={}", .{err});
|
log.err("error scheduling render timer err={}", .{err});
|
||||||
}
|
}
|
||||||
} else |_| {}
|
} else |_| {}
|
||||||
@ -927,7 +926,7 @@ fn keyCallback(
|
|||||||
|
|
||||||
.toggle_dev_mode => if (DevMode.enabled) {
|
.toggle_dev_mode => if (DevMode.enabled) {
|
||||||
DevMode.instance.visible = !DevMode.instance.visible;
|
DevMode.instance.visible = !DevMode.instance.visible;
|
||||||
win.render_timer.schedule() catch unreachable;
|
win.queueRender() catch unreachable;
|
||||||
} else log.warn("dev mode was not compiled into this binary", .{}),
|
} else log.warn("dev mode was not compiled into this binary", .{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1004,7 +1003,7 @@ fn focusCallback(window: glfw.Window, focused: bool) void {
|
|||||||
// We have to schedule a render because no matter what we're changing
|
// We have to schedule a render because no matter what we're changing
|
||||||
// the cursor. If we're focused its reappearing, if we're not then
|
// the cursor. If we're focused its reappearing, if we're not then
|
||||||
// its changing to hollow and not blinking.
|
// its changing to hollow and not blinking.
|
||||||
win.render_timer.schedule() catch unreachable;
|
win.queueRender() catch unreachable;
|
||||||
|
|
||||||
if (focused)
|
if (focused)
|
||||||
win.terminal_cursor.startTimer() catch unreachable
|
win.terminal_cursor.startTimer() catch unreachable
|
||||||
@ -1024,7 +1023,7 @@ fn refreshCallback(window: glfw.Window) void {
|
|||||||
const win = window.getUserPointer(Window) orelse return;
|
const win = window.getUserPointer(Window) orelse return;
|
||||||
|
|
||||||
// The point of this callback is to schedule a render, so do that.
|
// The point of this callback is to schedule a render, so do that.
|
||||||
win.render_timer.schedule() catch unreachable;
|
win.queueRender() catch unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
|
fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
|
||||||
@ -1036,7 +1035,7 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
|
|||||||
// If our dev mode window is visible then we always schedule a render on
|
// If our dev mode window is visible then we always schedule a render on
|
||||||
// cursor move because the cursor might touch our windows.
|
// cursor move because the cursor might touch our windows.
|
||||||
if (DevMode.enabled and DevMode.instance.visible) {
|
if (DevMode.enabled and DevMode.instance.visible) {
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render timer err={}", .{err});
|
log.err("error scheduling render timer err={}", .{err});
|
||||||
|
|
||||||
// If the mouse event was handled by imgui, ignore it.
|
// If the mouse event was handled by imgui, ignore it.
|
||||||
@ -1071,7 +1070,7 @@ fn scrollCallback(window: glfw.Window, xoff: f64, yoff: f64) void {
|
|||||||
// Schedule render since scrolling usually does something.
|
// Schedule render since scrolling usually does something.
|
||||||
// TODO(perf): we can only schedule render if we know scrolling
|
// TODO(perf): we can only schedule render if we know scrolling
|
||||||
// did something
|
// did something
|
||||||
win.render_timer.schedule() catch unreachable;
|
win.queueRender() catch unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of action to report for a mouse event.
|
/// The type of action to report for a mouse event.
|
||||||
@ -1256,7 +1255,7 @@ fn mouseButtonCallback(
|
|||||||
// If our dev mode window is visible then we always schedule a render on
|
// If our dev mode window is visible then we always schedule a render on
|
||||||
// cursor move because the cursor might touch our windows.
|
// cursor move because the cursor might touch our windows.
|
||||||
if (DevMode.enabled and DevMode.instance.visible) {
|
if (DevMode.enabled and DevMode.instance.visible) {
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render timer in cursorPosCallback err={}", .{err});
|
log.err("error scheduling render timer in cursorPosCallback err={}", .{err});
|
||||||
|
|
||||||
// If the mouse event was handled by imgui, ignore it.
|
// If the mouse event was handled by imgui, ignore it.
|
||||||
@ -1326,7 +1325,7 @@ fn mouseButtonCallback(
|
|||||||
// Selection is always cleared
|
// Selection is always cleared
|
||||||
if (win.terminal.selection != null) {
|
if (win.terminal.selection != null) {
|
||||||
win.terminal.selection = null;
|
win.terminal.selection = null;
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
|
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1345,7 +1344,7 @@ fn cursorPosCallback(
|
|||||||
// If our dev mode window is visible then we always schedule a render on
|
// If our dev mode window is visible then we always schedule a render on
|
||||||
// cursor move because the cursor might touch our windows.
|
// cursor move because the cursor might touch our windows.
|
||||||
if (DevMode.enabled and DevMode.instance.visible) {
|
if (DevMode.enabled and DevMode.instance.visible) {
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render timer in cursorPosCallback err={}", .{err});
|
log.err("error scheduling render timer in cursorPosCallback err={}", .{err});
|
||||||
|
|
||||||
// If the mouse event was handled by imgui, ignore it.
|
// If the mouse event was handled by imgui, ignore it.
|
||||||
@ -1380,7 +1379,7 @@ fn cursorPosCallback(
|
|||||||
if (win.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return;
|
if (win.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return;
|
||||||
|
|
||||||
// All roads lead to requiring a re-render at this pont.
|
// All roads lead to requiring a re-render at this pont.
|
||||||
win.render_timer.schedule() catch |err|
|
win.queueRender() catch |err|
|
||||||
log.err("error scheduling render timer in cursorPosCallback err={}", .{err});
|
log.err("error scheduling render timer in cursorPosCallback err={}", .{err});
|
||||||
|
|
||||||
// Convert to pixels from screen coords
|
// Convert to pixels from screen coords
|
||||||
@ -1533,7 +1532,7 @@ fn cursorTimerCallback(t: *libuv.Timer) void {
|
|||||||
|
|
||||||
// Swap blink state and schedule a render
|
// Swap blink state and schedule a render
|
||||||
win.renderer_state.cursor.blink = !win.renderer_state.cursor.blink;
|
win.renderer_state.cursor.blink = !win.renderer_state.cursor.blink;
|
||||||
win.render_timer.schedule() catch unreachable;
|
win.queueRender() catch unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 {
|
fn ttyReadAlloc(t: *libuv.Tty, size: usize) ?[]u8 {
|
||||||
@ -1582,7 +1581,7 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Schedule a render
|
// Schedule a render
|
||||||
win.render_timer.schedule() catch unreachable;
|
win.queueRender() catch unreachable;
|
||||||
|
|
||||||
// Process the terminal data. This is an extremely hot part of the
|
// Process the terminal data. This is an extremely hot part of the
|
||||||
// terminal emulator, so we do some abstraction leakage to avoid
|
// terminal emulator, so we do some abstraction leakage to avoid
|
||||||
@ -1639,21 +1638,6 @@ fn ttyWrite(req: *libuv.WriteReq, status: i32) void {
|
|||||||
//log.info("WROTE: {d}", .{status});
|
//log.info("WROTE: {d}", .{status});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn renderTimerCallback(t: *libuv.Timer) void {
|
|
||||||
const tracy = trace(@src());
|
|
||||||
tracy.color(0x006E7F); // blue-ish
|
|
||||||
defer tracy.end();
|
|
||||||
|
|
||||||
const win = t.getData(Window).?;
|
|
||||||
|
|
||||||
// Trigger a render
|
|
||||||
win.renderer_thread.wakeup.send() catch |err|
|
|
||||||
log.err("error sending render notification err={}", .{err});
|
|
||||||
|
|
||||||
// Record our run
|
|
||||||
win.render_timer.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
// Stream Callbacks
|
// Stream Callbacks
|
||||||
|
|
||||||
@ -1722,7 +1706,7 @@ pub fn eraseDisplay(self: *Window, mode: terminal.EraseDisplay) !void {
|
|||||||
if (mode == .complete) {
|
if (mode == .complete) {
|
||||||
// Whenever we erase the full display, scroll to bottom.
|
// Whenever we erase the full display, scroll to bottom.
|
||||||
try self.terminal.scrollViewport(.{ .bottom = {} });
|
try self.terminal.scrollViewport(.{ .bottom = {} });
|
||||||
try self.render_timer.schedule();
|
try self.queueRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.terminal.eraseDisplay(mode);
|
self.terminal.eraseDisplay(mode);
|
||||||
@ -1775,7 +1759,7 @@ pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void {
|
|||||||
self.terminal.modes.reverse_colors = enabled;
|
self.terminal.modes.reverse_colors = enabled;
|
||||||
|
|
||||||
// Schedule a render since we changed colors
|
// Schedule a render since we changed colors
|
||||||
try self.render_timer.schedule();
|
try self.queueRender();
|
||||||
},
|
},
|
||||||
|
|
||||||
.origin => {
|
.origin => {
|
||||||
@ -1803,7 +1787,7 @@ pub fn setMode(self: *Window, mode: terminal.Mode, enabled: bool) !void {
|
|||||||
self.terminal.primaryScreen(opts);
|
self.terminal.primaryScreen(opts);
|
||||||
|
|
||||||
// Schedule a render since we changed screens
|
// Schedule a render since we changed screens
|
||||||
try self.render_timer.schedule();
|
try self.queueRender();
|
||||||
},
|
},
|
||||||
|
|
||||||
.bracketed_paste => self.bracketed_paste = true,
|
.bracketed_paste => self.bracketed_paste = true,
|
||||||
|
@ -24,6 +24,9 @@ wakeup: libuv.Async,
|
|||||||
/// This can be used to stop the renderer on the next loop iteration.
|
/// This can be used to stop the renderer on the next loop iteration.
|
||||||
stop: libuv.Async,
|
stop: libuv.Async,
|
||||||
|
|
||||||
|
/// The timer used for rendering
|
||||||
|
render_h: libuv.Timer,
|
||||||
|
|
||||||
/// The windo we're rendering to.
|
/// The windo we're rendering to.
|
||||||
window: glfw.Window,
|
window: glfw.Window,
|
||||||
|
|
||||||
@ -54,7 +57,7 @@ pub fn init(
|
|||||||
loop.setData(allocPtr);
|
loop.setData(allocPtr);
|
||||||
|
|
||||||
// This async handle is used to "wake up" the renderer and force a render.
|
// This async handle is used to "wake up" the renderer and force a render.
|
||||||
var wakeup_h = try libuv.Async.init(alloc, loop, renderCallback);
|
var wakeup_h = try libuv.Async.init(alloc, loop, wakeupCallback);
|
||||||
errdefer wakeup_h.close((struct {
|
errdefer wakeup_h.close((struct {
|
||||||
fn callback(h: *libuv.Async) void {
|
fn callback(h: *libuv.Async) void {
|
||||||
const loop_alloc = h.loop().getData(Allocator).?.*;
|
const loop_alloc = h.loop().getData(Allocator).?.*;
|
||||||
@ -71,10 +74,20 @@ pub fn init(
|
|||||||
}
|
}
|
||||||
}).callback);
|
}).callback);
|
||||||
|
|
||||||
|
// The primary timer for rendering.
|
||||||
|
var render_h = try libuv.Timer.init(alloc, loop);
|
||||||
|
errdefer render_h.close((struct {
|
||||||
|
fn callback(h: *libuv.Timer) void {
|
||||||
|
const loop_alloc = h.loop().getData(Allocator).?.*;
|
||||||
|
h.deinit(loop_alloc);
|
||||||
|
}
|
||||||
|
}).callback);
|
||||||
|
|
||||||
return Thread{
|
return Thread{
|
||||||
.loop = loop,
|
.loop = loop,
|
||||||
.wakeup = wakeup_h,
|
.wakeup = wakeup_h,
|
||||||
.stop = stop_h,
|
.stop = stop_h,
|
||||||
|
.render_h = render_h,
|
||||||
.window = window,
|
.window = window,
|
||||||
.renderer = renderer_impl,
|
.renderer = renderer_impl,
|
||||||
.state = state,
|
.state = state,
|
||||||
@ -101,6 +114,12 @@ pub fn deinit(self: *Thread) void {
|
|||||||
h.deinit(handle_alloc);
|
h.deinit(handle_alloc);
|
||||||
}
|
}
|
||||||
}).callback);
|
}).callback);
|
||||||
|
self.render_h.close((struct {
|
||||||
|
fn callback(h: *libuv.Timer) void {
|
||||||
|
const handle_alloc = h.loop().getData(Allocator).?.*;
|
||||||
|
h.deinit(handle_alloc);
|
||||||
|
}
|
||||||
|
}).callback);
|
||||||
|
|
||||||
// Run the loop one more time, because destroying our other things
|
// Run the loop one more time, because destroying our other things
|
||||||
// like windows usually cancel all our event loop stuff and we need
|
// like windows usually cancel all our event loop stuff and we need
|
||||||
@ -124,6 +143,10 @@ pub fn threadMain(self: *Thread) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn threadMain_(self: *Thread) !void {
|
fn threadMain_(self: *Thread) !void {
|
||||||
|
// Get a copy to our allocator
|
||||||
|
// const alloc_ptr = self.loop.getData(Allocator).?;
|
||||||
|
// const alloc = alloc_ptr.*;
|
||||||
|
|
||||||
// Run our thread start/end callbacks. This is important because some
|
// Run our thread start/end callbacks. This is important because some
|
||||||
// renderers have to do per-thread setup. For example, OpenGL has to set
|
// renderers have to do per-thread setup. For example, OpenGL has to set
|
||||||
// some thread-local state since that is how it works.
|
// some thread-local state since that is how it works.
|
||||||
@ -135,13 +158,34 @@ fn threadMain_(self: *Thread) !void {
|
|||||||
self.wakeup.setData(self);
|
self.wakeup.setData(self);
|
||||||
defer self.wakeup.setData(null);
|
defer self.wakeup.setData(null);
|
||||||
|
|
||||||
|
// Set up our timer and start it for rendering
|
||||||
|
self.render_h.setData(self);
|
||||||
|
defer self.render_h.setData(null);
|
||||||
|
try self.wakeup.send();
|
||||||
|
|
||||||
// Run
|
// Run
|
||||||
log.debug("starting renderer thread", .{});
|
log.debug("starting renderer thread", .{});
|
||||||
defer log.debug("exiting renderer thread", .{});
|
defer log.debug("exiting renderer thread", .{});
|
||||||
_ = try self.loop.run(.default);
|
_ = try self.loop.run(.default);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn renderCallback(h: *libuv.Async) void {
|
fn wakeupCallback(h: *libuv.Async) void {
|
||||||
|
const t = h.getData(Thread) orelse {
|
||||||
|
// This shouldn't happen so we log it.
|
||||||
|
log.warn("render callback fired without data set", .{});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the timer is already active then we don't have to do anything.
|
||||||
|
const active = t.render_h.isActive() catch true;
|
||||||
|
if (active) return;
|
||||||
|
|
||||||
|
// Timer is not active, let's start it
|
||||||
|
t.render_h.start(renderCallback, 10, 0) catch |err|
|
||||||
|
log.warn("render timer failed to start err={}", .{err});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renderCallback(h: *libuv.Timer) void {
|
||||||
const t = h.getData(Thread) orelse {
|
const t = h.getData(Thread) orelse {
|
||||||
// This shouldn't happen so we log it.
|
// This shouldn't happen so we log it.
|
||||||
log.warn("render callback fired without data set", .{});
|
log.warn("render callback fired without data set", .{});
|
||||||
|
Reference in New Issue
Block a user