mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
use a font atlas!
This commit is contained in:
12
shaders/text-atlas.f.glsl
Normal file
12
shaders/text-atlas.f.glsl
Normal file
@ -0,0 +1,12 @@
|
||||
#version 330 core
|
||||
|
||||
in vec2 TexCoords;
|
||||
in vec4 VertexColor;
|
||||
|
||||
uniform sampler2D text;
|
||||
|
||||
void main()
|
||||
{
|
||||
float a = texture(text, TexCoords).r;
|
||||
gl_FragColor = vec4(VertexColor.rgb, VertexColor.a*a);
|
||||
}
|
17
shaders/text-atlas.v.glsl
Normal file
17
shaders/text-atlas.v.glsl
Normal file
@ -0,0 +1,17 @@
|
||||
#version 330 core
|
||||
|
||||
layout (location = 0) in vec3 vertex;
|
||||
layout (location = 1) in vec2 tex_coord;
|
||||
layout (location = 2) in vec4 color;
|
||||
|
||||
out vec2 TexCoords;
|
||||
out vec4 VertexColor;
|
||||
|
||||
uniform mat4 projection;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = projection * vec4(vertex, 1.0);
|
||||
TexCoords = tex_coord.xy;
|
||||
VertexColor = color;
|
||||
}
|
@ -74,7 +74,8 @@ pub fn run(self: App) !void {
|
||||
gl.clearColor(0.2, 0.3, 0.3, 1.0);
|
||||
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
try self.text.render("sh $ /bin/bash -c \"echo hello\"", 25.0, 25.0, 1.0, .{ 0.5, 0.8, 0.2 });
|
||||
try self.text.render("sh $ /bin/bash -c \"echo hello\"", 25.0, 25.0, .{ 0.5, 0.8, 0.2 });
|
||||
//try self.text.render("hi", 25.0, 25.0, .{ 0.5, 0.8, 0.2 });
|
||||
|
||||
try self.window.swapBuffers();
|
||||
try glfw.waitEvents();
|
||||
|
@ -4,15 +4,15 @@ const std = @import("std");
|
||||
const ftc = @import("freetype/c.zig");
|
||||
const gl = @import("opengl.zig");
|
||||
const gb = @import("gb_math.zig");
|
||||
const ftgl = @import("freetype-gl/c.zig");
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
ft: ftc.FT_Library,
|
||||
face: ftc.FT_Face,
|
||||
chars: CharList,
|
||||
vao: gl.VertexArray = undefined,
|
||||
vbo: gl.Buffer = undefined,
|
||||
program: gl.Program = undefined,
|
||||
projection: gb.gbMat4 = undefined,
|
||||
font: *ftgl.texture_font_t,
|
||||
atlas: *ftgl.texture_atlas_t,
|
||||
|
||||
program: gl.Program,
|
||||
tex: gl.Texture,
|
||||
|
||||
const CharList = std.ArrayListUnmanaged(Char);
|
||||
const Char = struct {
|
||||
@ -23,96 +23,58 @@ const Char = struct {
|
||||
};
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !TextRenderer {
|
||||
var ft: ftc.FT_Library = undefined;
|
||||
if (ftc.FT_Init_FreeType(&ft) != 0) {
|
||||
return error.FreetypeInitFailed;
|
||||
}
|
||||
|
||||
var face: ftc.FT_Face = undefined;
|
||||
if (ftc.FT_New_Memory_Face(
|
||||
ft,
|
||||
const atlas = ftgl.texture_atlas_new(512, 512, 1);
|
||||
if (atlas == null) return error.FontAtlasFail;
|
||||
errdefer ftgl.texture_atlas_delete(atlas);
|
||||
const font = ftgl.texture_font_new_from_memory(
|
||||
atlas,
|
||||
48,
|
||||
face_ttf,
|
||||
face_ttf.len,
|
||||
0,
|
||||
&face,
|
||||
) != 0) {
|
||||
return error.FreetypeFaceFailed;
|
||||
}
|
||||
);
|
||||
if (font == null) return error.FontInitFail;
|
||||
errdefer ftgl.texture_font_delete(font);
|
||||
|
||||
_ = ftc.FT_Set_Pixel_Sizes(face, 0, 48);
|
||||
|
||||
// disable byte-alignment restriction
|
||||
try gl.pixelStore(gl.c.GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
// Pre-render all the ASCII characters
|
||||
var chars = try CharList.initCapacity(alloc, 128);
|
||||
var i: usize = 0;
|
||||
while (i < chars.capacity) : (i += 1) {
|
||||
// Load all visible ASCII characters.
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
// Load the character
|
||||
if (ftc.FT_Load_Char(face, i, ftc.FT_LOAD_RENDER) != 0) {
|
||||
if (ftgl.texture_font_load_glyph(font, &i) == 0) {
|
||||
return error.GlyphLoadFailed;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the texture
|
||||
// Build our texture
|
||||
const tex = try gl.Texture.create();
|
||||
var binding = try tex.bind(.@"2D");
|
||||
defer binding.unbind();
|
||||
try binding.image2D(
|
||||
0,
|
||||
.Red,
|
||||
@intCast(c_int, face.*.glyph.*.bitmap.width),
|
||||
@intCast(c_int, face.*.glyph.*.bitmap.rows),
|
||||
0,
|
||||
.Red,
|
||||
.UnsignedByte,
|
||||
face.*.glyph.*.bitmap.buffer,
|
||||
);
|
||||
errdefer tex.destroy();
|
||||
const binding = try tex.bind(.@"2D");
|
||||
try binding.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
||||
try binding.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
||||
try binding.parameter(.MinFilter, gl.c.GL_LINEAR);
|
||||
try binding.parameter(.MagFilter, gl.c.GL_LINEAR);
|
||||
|
||||
// Store the character
|
||||
chars.appendAssumeCapacity(.{
|
||||
.tex = tex,
|
||||
.size = .{
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap.width),
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap.rows),
|
||||
},
|
||||
.bearing = .{
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap_left),
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap_top),
|
||||
},
|
||||
.advance = @intCast(c_uint, face.*.glyph.*.advance.x),
|
||||
});
|
||||
}
|
||||
|
||||
// Configure VAO/VBO for glyph rendering
|
||||
const vao = try gl.VertexArray.create();
|
||||
const vbo = try gl.Buffer.create();
|
||||
try vao.bind();
|
||||
var binding = try vbo.bind(.ArrayBuffer);
|
||||
try binding.setDataNull([6 * 4]f32, .DynamicDraw);
|
||||
try binding.enableVertexAttribArray(0);
|
||||
try binding.vertexAttribPointer(0, 4, gl.c.GL_FLOAT, false, 4 * @sizeOf(f32), null);
|
||||
binding.unbind();
|
||||
try gl.VertexArray.unbind();
|
||||
try binding.image2D(
|
||||
0,
|
||||
.Red,
|
||||
@intCast(c_int, atlas.*.width),
|
||||
@intCast(c_int, atlas.*.height),
|
||||
0,
|
||||
.Red,
|
||||
.UnsignedByte,
|
||||
atlas.*.data,
|
||||
);
|
||||
|
||||
// Create our shader
|
||||
const program = try gl.Program.createVF(
|
||||
@embedFile("../shaders/text.v.glsl"),
|
||||
@embedFile("../shaders/text.f.glsl"),
|
||||
@embedFile("../shaders/text-atlas.v.glsl"),
|
||||
@embedFile("../shaders/text-atlas.f.glsl"),
|
||||
);
|
||||
|
||||
var res = TextRenderer{
|
||||
.alloc = alloc,
|
||||
.ft = ft,
|
||||
.face = face,
|
||||
.chars = chars,
|
||||
.font = font,
|
||||
.atlas = atlas,
|
||||
.program = program,
|
||||
.vao = vao,
|
||||
.vbo = vbo,
|
||||
.projection = undefined,
|
||||
.tex = tex,
|
||||
};
|
||||
|
||||
// Update the initialize size so we have some projection. We
|
||||
@ -123,14 +85,8 @@ pub fn init(alloc: std.mem.Allocator) !TextRenderer {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TextRenderer) void {
|
||||
// TODO: delete textures
|
||||
self.chars.deinit(self.alloc);
|
||||
|
||||
if (ftc.FT_Done_Face(self.face) != 0)
|
||||
std.log.err("freetype face deinitialization failed", .{});
|
||||
if (ftc.FT_Done_FreeType(self.ft) != 0)
|
||||
std.log.err("freetype library deinitialization failed", .{});
|
||||
|
||||
ftgl.texture_font_delete(self.font);
|
||||
ftgl.texture_atlas_delete(self.atlas);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -152,44 +108,74 @@ pub fn render(
|
||||
text: []const u8,
|
||||
x: f32,
|
||||
y: f32,
|
||||
scale: f32,
|
||||
color: @Vector(3, f32),
|
||||
) !void {
|
||||
try self.program.use();
|
||||
try self.program.setUniform("textColor", color);
|
||||
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
||||
try self.vao.bind();
|
||||
const r = color[0];
|
||||
const g = color[1];
|
||||
const b = color[2];
|
||||
const a: f32 = 1.0;
|
||||
|
||||
var vertices: std.ArrayListUnmanaged([6][9]f32) = .{};
|
||||
try vertices.ensureUnusedCapacity(self.alloc, text.len);
|
||||
defer vertices.deinit(self.alloc);
|
||||
|
||||
var curx: f32 = x;
|
||||
for (text) |c| {
|
||||
const char = self.chars.items[c];
|
||||
if (ftgl.texture_font_get_glyph(self.font, &c)) |glyph_ptr| {
|
||||
const glyph = glyph_ptr.*;
|
||||
const kerning = 0; // for now
|
||||
curx += kerning;
|
||||
|
||||
const xpos = curx + (char.bearing[0] * scale);
|
||||
const ypos = y - ((char.size[1] - char.bearing[1]) * scale);
|
||||
const w = char.size[0] * scale;
|
||||
const h = char.size[1] * scale;
|
||||
const x0 = curx + @intToFloat(f32, glyph.offset_x);
|
||||
const y0 = y + @intToFloat(f32, glyph.offset_y);
|
||||
const x1 = x0 + @intToFloat(f32, glyph.width);
|
||||
const y1 = y0 - @intToFloat(f32, glyph.height);
|
||||
const s0 = glyph.s0;
|
||||
const t0 = glyph.t0;
|
||||
const s1 = glyph.s1;
|
||||
const t1 = glyph.t1;
|
||||
|
||||
const vert = [6][4]f32{
|
||||
.{ xpos, ypos + h, 0.0, 0.0 },
|
||||
.{ xpos, ypos, 0.0, 1.0 },
|
||||
.{ xpos + w, ypos, 1.0, 1.0 },
|
||||
std.log.info("CHAR ch={} x0={} y0={} x1={} y1={}", .{ c, x0, y0, x1, y1 });
|
||||
|
||||
.{ xpos, ypos + h, 0.0, 0.0 },
|
||||
.{ xpos + w, ypos, 1.0, 1.0 },
|
||||
.{ xpos + w, ypos + h, 1.0, 0.0 },
|
||||
const vert = [6][9]f32{
|
||||
.{ x0, y0, 0, s0, t0, r, g, b, a },
|
||||
.{ x0, y1, 0, s0, t1, r, g, b, a },
|
||||
.{ x1, y1, 0, s1, t1, r, g, b, a },
|
||||
.{ x0, y0, 0, s0, t0, r, g, b, a },
|
||||
.{ x1, y1, 0, s1, t1, r, g, b, a },
|
||||
.{ x1, y0, 0, s1, t0, r, g, b, a },
|
||||
};
|
||||
|
||||
var texbind = try char.tex.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
var bind = try self.vbo.bind(.ArrayBuffer);
|
||||
try bind.setSubData(0, vert);
|
||||
bind.unbind();
|
||||
vertices.appendAssumeCapacity(vert);
|
||||
|
||||
try gl.drawArrays(gl.c.GL_TRIANGLES, 0, 6);
|
||||
|
||||
curx += @intToFloat(f32, char.advance >> 6) * scale;
|
||||
curx += glyph.advance_x;
|
||||
}
|
||||
}
|
||||
|
||||
try self.program.use();
|
||||
|
||||
// Bind our texture and set our data
|
||||
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
||||
var texbind = try self.tex.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
|
||||
// Configure VAO/VBO for glyph rendering
|
||||
const vao = try gl.VertexArray.create();
|
||||
defer vao.destroy();
|
||||
try vao.bind();
|
||||
const vbo = try gl.Buffer.create();
|
||||
defer vbo.destroy();
|
||||
var binding = try vbo.bind(.ArrayBuffer);
|
||||
defer binding.unbind();
|
||||
try binding.setData(vertices.items, .DynamicDraw);
|
||||
try binding.enableVertexAttribArray(0);
|
||||
try binding.vertexAttribPointer(0, 3, gl.c.GL_FLOAT, false, 9 * @sizeOf(f32), null);
|
||||
try binding.enableVertexAttribArray(1);
|
||||
try binding.vertexAttribPointer(1, 2, gl.c.GL_FLOAT, false, 9 * @sizeOf(f32), @intToPtr(*const anyopaque, 3 * @sizeOf(f32)));
|
||||
try binding.enableVertexAttribArray(2);
|
||||
try binding.vertexAttribPointer(2, 4, gl.c.GL_FLOAT, false, 9 * @sizeOf(f32), @intToPtr(*const anyopaque, 5 * @sizeOf(f32)));
|
||||
|
||||
try gl.drawArrays(gl.c.GL_TRIANGLES, 0, @intCast(c_int, vertices.items.len * 6));
|
||||
try gl.VertexArray.unbind();
|
||||
}
|
||||
|
||||
|
196
src/TextRenderer2.zig
Normal file
196
src/TextRenderer2.zig
Normal file
@ -0,0 +1,196 @@
|
||||
const TextRenderer = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const ftc = @import("freetype/c.zig");
|
||||
const gl = @import("opengl.zig");
|
||||
const gb = @import("gb_math.zig");
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
ft: ftc.FT_Library,
|
||||
face: ftc.FT_Face,
|
||||
chars: CharList,
|
||||
vao: gl.VertexArray = undefined,
|
||||
vbo: gl.Buffer = undefined,
|
||||
program: gl.Program = undefined,
|
||||
projection: gb.gbMat4 = undefined,
|
||||
|
||||
const CharList = std.ArrayListUnmanaged(Char);
|
||||
const Char = struct {
|
||||
tex: gl.Texture,
|
||||
size: @Vector(2, f32),
|
||||
bearing: @Vector(2, f32),
|
||||
advance: c_uint,
|
||||
};
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !TextRenderer {
|
||||
var ft: ftc.FT_Library = undefined;
|
||||
if (ftc.FT_Init_FreeType(&ft) != 0) {
|
||||
return error.FreetypeInitFailed;
|
||||
}
|
||||
|
||||
var face: ftc.FT_Face = undefined;
|
||||
if (ftc.FT_New_Memory_Face(
|
||||
ft,
|
||||
face_ttf,
|
||||
face_ttf.len,
|
||||
0,
|
||||
&face,
|
||||
) != 0) {
|
||||
return error.FreetypeFaceFailed;
|
||||
}
|
||||
|
||||
_ = ftc.FT_Set_Pixel_Sizes(face, 0, 48);
|
||||
|
||||
// disable byte-alignment restriction
|
||||
try gl.pixelStore(gl.c.GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
// Pre-render all the ASCII characters
|
||||
var chars = try CharList.initCapacity(alloc, 128);
|
||||
var i: usize = 0;
|
||||
while (i < chars.capacity) : (i += 1) {
|
||||
// Load the character
|
||||
if (ftc.FT_Load_Char(face, i, ftc.FT_LOAD_RENDER) != 0) {
|
||||
return error.GlyphLoadFailed;
|
||||
}
|
||||
|
||||
// Generate the texture
|
||||
const tex = try gl.Texture.create();
|
||||
var binding = try tex.bind(.@"2D");
|
||||
defer binding.unbind();
|
||||
try binding.image2D(
|
||||
0,
|
||||
.Red,
|
||||
@intCast(c_int, face.*.glyph.*.bitmap.width),
|
||||
@intCast(c_int, face.*.glyph.*.bitmap.rows),
|
||||
0,
|
||||
.Red,
|
||||
.UnsignedByte,
|
||||
face.*.glyph.*.bitmap.buffer,
|
||||
);
|
||||
try binding.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
|
||||
try binding.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
|
||||
try binding.parameter(.MinFilter, gl.c.GL_LINEAR);
|
||||
try binding.parameter(.MagFilter, gl.c.GL_LINEAR);
|
||||
|
||||
// Store the character
|
||||
chars.appendAssumeCapacity(.{
|
||||
.tex = tex,
|
||||
.size = .{
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap.width),
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap.rows),
|
||||
},
|
||||
.bearing = .{
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap_left),
|
||||
@intToFloat(f32, face.*.glyph.*.bitmap_top),
|
||||
},
|
||||
.advance = @intCast(c_uint, face.*.glyph.*.advance.x),
|
||||
});
|
||||
}
|
||||
|
||||
// Configure VAO/VBO for glyph rendering
|
||||
const vao = try gl.VertexArray.create();
|
||||
const vbo = try gl.Buffer.create();
|
||||
try vao.bind();
|
||||
var binding = try vbo.bind(.ArrayBuffer);
|
||||
try binding.setDataNull([6 * 4]f32, .DynamicDraw);
|
||||
try binding.enableVertexAttribArray(0);
|
||||
try binding.vertexAttribPointer(0, 4, gl.c.GL_FLOAT, false, 4 * @sizeOf(f32), null);
|
||||
binding.unbind();
|
||||
try gl.VertexArray.unbind();
|
||||
|
||||
// Create our shader
|
||||
const program = try gl.Program.createVF(
|
||||
@embedFile("../shaders/text.v.glsl"),
|
||||
@embedFile("../shaders/text.f.glsl"),
|
||||
);
|
||||
|
||||
var res = TextRenderer{
|
||||
.alloc = alloc,
|
||||
.ft = ft,
|
||||
.face = face,
|
||||
.chars = chars,
|
||||
.program = program,
|
||||
.vao = vao,
|
||||
.vbo = vbo,
|
||||
.projection = undefined,
|
||||
};
|
||||
|
||||
// Update the initialize size so we have some projection. We
|
||||
// expect this will get updated almost immediately.
|
||||
try res.setScreenSize(3000, 1666);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TextRenderer) void {
|
||||
// TODO: delete textures
|
||||
self.chars.deinit(self.alloc);
|
||||
|
||||
if (ftc.FT_Done_Face(self.face) != 0)
|
||||
std.log.err("freetype face deinitialization failed", .{});
|
||||
if (ftc.FT_Done_FreeType(self.ft) != 0)
|
||||
std.log.err("freetype library deinitialization failed", .{});
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub fn setScreenSize(self: *TextRenderer, w: i32, h: i32) !void {
|
||||
gb.gb_mat4_ortho2d(
|
||||
&self.projection,
|
||||
0,
|
||||
@intToFloat(f32, w),
|
||||
0,
|
||||
@intToFloat(f32, h),
|
||||
);
|
||||
|
||||
try self.program.use();
|
||||
try self.program.setUniform("projection", self.projection);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
self: TextRenderer,
|
||||
text: []const u8,
|
||||
x: f32,
|
||||
y: f32,
|
||||
scale: f32,
|
||||
color: @Vector(3, f32),
|
||||
) !void {
|
||||
try self.program.use();
|
||||
try self.program.setUniform("textColor", color);
|
||||
try gl.Texture.active(gl.c.GL_TEXTURE0);
|
||||
try self.vao.bind();
|
||||
|
||||
var curx: f32 = x;
|
||||
for (text) |c| {
|
||||
const char = self.chars.items[c];
|
||||
|
||||
const xpos = curx + (char.bearing[0] * scale);
|
||||
const ypos = y - ((char.size[1] - char.bearing[1]) * scale);
|
||||
const w = char.size[0] * scale;
|
||||
const h = char.size[1] * scale;
|
||||
|
||||
const vert = [6][4]f32{
|
||||
.{ xpos, ypos + h, 0.0, 0.0 },
|
||||
.{ xpos, ypos, 0.0, 1.0 },
|
||||
.{ xpos + w, ypos, 1.0, 1.0 },
|
||||
|
||||
.{ xpos, ypos + h, 0.0, 0.0 },
|
||||
.{ xpos + w, ypos, 1.0, 1.0 },
|
||||
.{ xpos + w, ypos + h, 1.0, 0.0 },
|
||||
};
|
||||
|
||||
var texbind = try char.tex.bind(.@"2D");
|
||||
defer texbind.unbind();
|
||||
var bind = try self.vbo.bind(.ArrayBuffer);
|
||||
try bind.setSubData(0, vert);
|
||||
bind.unbind();
|
||||
|
||||
try gl.drawArrays(gl.c.GL_TRIANGLES, 0, 6);
|
||||
|
||||
curx += @intToFloat(f32, char.advance >> 6) * scale;
|
||||
}
|
||||
|
||||
try gl.VertexArray.unbind();
|
||||
}
|
||||
|
||||
const face_ttf = @embedFile("../fonts/Inconsolata-Regular.ttf");
|
@ -24,6 +24,8 @@ pub fn link(
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
try flags.appendSlice(&.{
|
||||
"-std=c99",
|
||||
"-O0",
|
||||
"-DGL_WITH_GLAD",
|
||||
});
|
||||
|
||||
|
4
src/freetype-gl/c.zig
Normal file
4
src/freetype-gl/c.zig
Normal file
@ -0,0 +1,4 @@
|
||||
pub usingnamespace @cImport({
|
||||
@cInclude("texture-atlas.h");
|
||||
@cInclude("texture-font.h");
|
||||
});
|
@ -40,6 +40,9 @@ pub const Binding = struct {
|
||||
usage: Usage,
|
||||
) !void {
|
||||
const info = dataInfo(data);
|
||||
std.log.info("SET DATA {}", .{
|
||||
info.size,
|
||||
});
|
||||
c.glBufferData(@enumToInt(b.target), info.size, info.ptr, @enumToInt(usage));
|
||||
try errors.getError();
|
||||
}
|
||||
@ -83,7 +86,7 @@ pub const Binding = struct {
|
||||
.ptr = data,
|
||||
},
|
||||
.Slice => .{
|
||||
.size = @sizeOf(ptr.child) * data.len,
|
||||
.size = @intCast(isize, @sizeOf(ptr.child) * data.len),
|
||||
.ptr = data.ptr,
|
||||
},
|
||||
else => {
|
||||
|
Reference in New Issue
Block a user