Compare commits
No commits in common. "fa4d3ed0ece4aaede5f14c60119e26863c56b51c" and "0db858e814fe0dab7326ec577b883cc9f7481ab9" have entirely different histories.
fa4d3ed0ec
...
0db858e814
13
TODO
13
TODO
@ -1,6 +1,4 @@
|
|||||||
# TODOs, semi-random sorting
|
# TODOs, semi-random sorting
|
||||||
|
|
||||||
[ ] Check every instance of foreach to see if I am using by-copy or by-reference correctly
|
|
||||||
[x] Implement glyph draw command
|
[x] Implement glyph draw command
|
||||||
[x] Implement div.view and scrollbars
|
[x] Implement div.view and scrollbars
|
||||||
[ ] Port font system from C to C3 (rewrite1)
|
[ ] Port font system from C to C3 (rewrite1)
|
||||||
@ -10,32 +8,25 @@
|
|||||||
[ ] Do not redraw if there was no update (no layout and no draw)
|
[ ] Do not redraw if there was no update (no layout and no draw)
|
||||||
[ ] Better handling of the active and focused widgets, try
|
[ ] Better handling of the active and focused widgets, try
|
||||||
to maintain focus until mouse release (fix scroll bars)
|
to maintain focus until mouse release (fix scroll bars)
|
||||||
[ ] Write a description for each file and the structs, interfaces provided
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
[x] rect commads should have:
|
[x] rect commads should have:
|
||||||
_ border width
|
* border width
|
||||||
_ border radius
|
* border radius
|
||||||
[x] add a command to update an atlas
|
[x] add a command to update an atlas
|
||||||
|
|
||||||
## Atlases
|
## Atlases
|
||||||
|
|
||||||
[ ] Add an interface to create, destroy, update and get atlases based on their ids
|
[ ] Add an interface to create, destroy, update and get atlases based on their ids
|
||||||
[ ] Implement multiple font atlases
|
[ ] Implement multiple font atlases
|
||||||
[ ] Create and use ShortIds for atlases
|
|
||||||
|
|
||||||
## Fonts
|
## Fonts
|
||||||
|
|
||||||
[ ] Fix the missing alpha channel
|
[ ] Fix the missing alpha channel
|
||||||
[x] Fix the alignment
|
[x] Fix the alignment
|
||||||
|
|
||||||
## Raylib
|
## Raylib
|
||||||
|
|
||||||
[ ] Implement type (Rect, Color, Point) conversion functions between rl:: and ugui::
|
[ ] Implement type (Rect, Color, Point) conversion functions between rl:: and ugui::
|
||||||
[x] Implement pixel radius rounding for border radius
|
[x] Implement pixel radius rounding for border radius
|
||||||
|
|
||||||
## Widgets
|
## Widgets
|
||||||
|
|
||||||
[ ] Dynamic text box to implement an fps counter
|
[ ] Dynamic text box to implement an fps counter
|
||||||
[ ] Button with label
|
[ ] Button with label
|
||||||
|
@ -27,7 +27,7 @@ fn int main(String[] args)
|
|||||||
{
|
{
|
||||||
ugui::Ctx ui;
|
ugui::Ctx ui;
|
||||||
ui.init()!!;
|
ui.init()!!;
|
||||||
ui.font.load("/usr/share/fonts/TTF/HackNerdFontMono-Regular.ttf", 16)!!;
|
ui.font.load("/usr/share/fonts/NerdFonts/ttf/HackNerdFont-Regular.ttf", 16, scale: 1.5)!!;
|
||||||
|
|
||||||
short width = 800;
|
short width = 800;
|
||||||
short height = 450;
|
short height = 450;
|
||||||
|
@ -1,183 +0,0 @@
|
|||||||
module ugui;
|
|
||||||
|
|
||||||
import std::core::mem;
|
|
||||||
import std::math::random;
|
|
||||||
|
|
||||||
random::SimpleRandom atlas_seed;
|
|
||||||
|
|
||||||
fault UgAtlasError {
|
|
||||||
CANNOT_PLACE,
|
|
||||||
INVALID_FORMAT,
|
|
||||||
MISSING_ATLAS,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AtlasFormat {
|
|
||||||
ATLAS_GRAYSCALE,
|
|
||||||
ATLAS_ALPHA,
|
|
||||||
}
|
|
||||||
|
|
||||||
// black and white atlas
|
|
||||||
struct Atlas {
|
|
||||||
AtlasFormat format;
|
|
||||||
Id id;
|
|
||||||
ushort width, height;
|
|
||||||
char[] buffer;
|
|
||||||
|
|
||||||
Point row;
|
|
||||||
ushort row_h;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: allocate atlases dynamically
|
|
||||||
fn void! Ctx.init_atlases(&ctx)
|
|
||||||
{
|
|
||||||
atlas_seed.set_seed("ciao mamma");
|
|
||||||
ctx.atlas = mem::new_array(Atlas, MAX_ATLAS);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn void Ctx.free_atlases(&ctx)
|
|
||||||
{
|
|
||||||
foreach (a: ctx.atlas) {
|
|
||||||
a.free();
|
|
||||||
}
|
|
||||||
mem::free(ctx.atlas);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: this could be slow
|
|
||||||
fn Atlas*! Ctx.get_atlas(&ctx, Id id)
|
|
||||||
{
|
|
||||||
foreach (i, a: ctx.atlas) {
|
|
||||||
if (a.id == id) {
|
|
||||||
return &ctx.atlas[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return UgAtlasError.MISSING_ATLAS?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use the same allocator as all the other parts of the code
|
|
||||||
fn Atlas*! Ctx.request_new_atlas(&ctx, AtlasFormat format, Id *rid, ushort width, ushort height)
|
|
||||||
{
|
|
||||||
usz idx;
|
|
||||||
bool free = false;
|
|
||||||
for (; idx < ctx.atlas.len; idx++) {
|
|
||||||
if (ctx.atlas[idx].id == 0) {
|
|
||||||
free = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (free) {
|
|
||||||
*rid = ctx.unique_atlas_id();
|
|
||||||
ctx.atlas[idx].new(format, *rid, width, height)!;
|
|
||||||
return &ctx.atlas[idx];
|
|
||||||
} else {
|
|
||||||
// TODO: reallocate the atlas vector
|
|
||||||
return UgAtlasError.MISSING_ATLAS?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: WTF
|
|
||||||
fn Id Ctx.unique_atlas_id(&ctx) {
|
|
||||||
Id id = atlas_seed.next_long();
|
|
||||||
bool ok = true;
|
|
||||||
foreach (a: ctx.atlas) {
|
|
||||||
if (a.id == id) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok ? id : ctx.unique_atlas_id();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn void Ctx.discard_atlas(&ctx, Id id)
|
|
||||||
{
|
|
||||||
foreach (&a: ctx.atlas) {
|
|
||||||
if (a.id == id) {
|
|
||||||
a.free();
|
|
||||||
*a = Atlas{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to place a sprite into an atlas of the correct type and return it's id
|
|
||||||
// new_width and new_height are used in case a new atlas has to be requested, in that
|
|
||||||
// case they are used as width and height for the new atlas
|
|
||||||
// FIXME: maybe there has to be a default value
|
|
||||||
fn Id! Ctx.place_sprite(&ctx, Point* uv, AtlasFormat format, char[] pixels, ushort w, ushort h, ushort new_width, ushort new_height)
|
|
||||||
{
|
|
||||||
// try to place the sprite into an existing atlas
|
|
||||||
foreach (&a: ctx.atlas) {
|
|
||||||
if (a.id != 0 && a.format == format) {
|
|
||||||
Point! ouv = a.place(pixels, w, h);
|
|
||||||
if (catch ouv) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
*uv = ouv;
|
|
||||||
return a.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fail... try to request a new atlas and place it there
|
|
||||||
Id id;
|
|
||||||
Atlas* atlas = ctx.request_new_atlas(format, &id, new_width, new_height)!;
|
|
||||||
*uv = atlas.place(pixels, w, h)!;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn void! Atlas.new(&atlas, AtlasFormat format, Id id, ushort width, ushort height)
|
|
||||||
{
|
|
||||||
atlas.format = format;
|
|
||||||
atlas.id = id;
|
|
||||||
atlas.width = width;
|
|
||||||
atlas.height = height;
|
|
||||||
|
|
||||||
usz bpp = 1;
|
|
||||||
switch (format) {
|
|
||||||
case ATLAS_GRAYSCALE: nextcase;
|
|
||||||
case ATLAS_ALPHA:
|
|
||||||
bpp = 1;
|
|
||||||
default:
|
|
||||||
return UgAtlasError.INVALID_FORMAT?;
|
|
||||||
}
|
|
||||||
usz size = ((usz)atlas.width*atlas.height) * bpp;
|
|
||||||
atlas.buffer = mem::new_array(char, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn void Atlas.free(&atlas)
|
|
||||||
{
|
|
||||||
free(atlas.buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// place a rect inside the atlas
|
|
||||||
// uses a row first algorithm
|
|
||||||
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
|
|
||||||
// FIXME: untested with non-1bpp buffer depths
|
|
||||||
fn Point! Atlas.place(&atlas, char[] pixels, ushort w, ushort h)
|
|
||||||
{
|
|
||||||
Point p;
|
|
||||||
|
|
||||||
// TODO: simplify this and use rect_collision() and such
|
|
||||||
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
|
|
||||||
p = atlas.row;
|
|
||||||
} else {
|
|
||||||
atlas.row.x = 0;
|
|
||||||
atlas.row.y = atlas.row.y + atlas.row_h;
|
|
||||||
atlas.row_h = 0;
|
|
||||||
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
|
|
||||||
p = atlas.row;
|
|
||||||
} else {
|
|
||||||
return UgAtlasError.CANNOT_PLACE?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (usz y = 0; y < h; y++) {
|
|
||||||
for (usz x = 0; x < w; x++) {
|
|
||||||
atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x)] = pixels[y*w + x];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
atlas.row.x += w;
|
|
||||||
if (h > atlas.row_h) {
|
|
||||||
atlas.row_h = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}
|
|
@ -127,7 +127,6 @@ const uint STACK_STEP = 10;
|
|||||||
const uint MAX_ELEMS = 128;
|
const uint MAX_ELEMS = 128;
|
||||||
const uint MAX_CMDS = 256;
|
const uint MAX_CMDS = 256;
|
||||||
const uint ROOT_ID = 1;
|
const uint ROOT_ID = 1;
|
||||||
const uint MAX_ATLAS = 2;
|
|
||||||
|
|
||||||
// command type
|
// command type
|
||||||
enum CmdType {
|
enum CmdType {
|
||||||
@ -194,8 +193,6 @@ struct Ctx {
|
|||||||
// total size in pixels of the context
|
// total size in pixels of the context
|
||||||
ushort width, height;
|
ushort width, height;
|
||||||
Style style;
|
Style style;
|
||||||
|
|
||||||
Atlas[] atlas;
|
|
||||||
Font font;
|
Font font;
|
||||||
|
|
||||||
bool has_focus;
|
bool has_focus;
|
||||||
|
@ -34,7 +34,7 @@ struct Glyph {
|
|||||||
ushort u, v;
|
ushort u, v;
|
||||||
ushort w, h;
|
ushort w, h;
|
||||||
short adv, ox, oy;
|
short adv, ox, oy;
|
||||||
Id atlas_id;
|
short idx; // atlas index
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint FONT_CACHED = 512;
|
const uint FONT_CACHED = 512;
|
||||||
@ -47,6 +47,19 @@ fault UgFontError {
|
|||||||
RENDER_ERROR,
|
RENDER_ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fault UgAtlasError {
|
||||||
|
CANNOT_PLACE,
|
||||||
|
}
|
||||||
|
|
||||||
|
// black and white atlas
|
||||||
|
struct AtlasBW {
|
||||||
|
ushort width, height;
|
||||||
|
char[] buffer;
|
||||||
|
|
||||||
|
Point row;
|
||||||
|
ushort row_h;
|
||||||
|
}
|
||||||
|
|
||||||
struct Font {
|
struct Font {
|
||||||
schrift::Sft sft;
|
schrift::Sft sft;
|
||||||
String path;
|
String path;
|
||||||
@ -54,12 +67,57 @@ struct Font {
|
|||||||
|
|
||||||
float size;
|
float size;
|
||||||
float ascender, descender, linegap; // Line Metrics
|
float ascender, descender, linegap; // Line Metrics
|
||||||
|
AtlasBW[] atlas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn void! AtlasBW.new(&atlas, ushort width, ushort height)
|
||||||
fn void! Ctx.font_load(&cxt, String path, uint height, float scale = 1)
|
{
|
||||||
|
atlas.width = width;
|
||||||
|
atlas.height = height;
|
||||||
|
atlas.buffer = mem::new_array(char, (usz)atlas.width*atlas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void AtlasBW.free(&atlas)
|
||||||
|
{
|
||||||
|
free(atlas.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// place a rect inside the atlas
|
||||||
|
// uses a row first algorithm
|
||||||
|
// TODO: use a skyline algorithm https://jvernay.fr/en/blog/skyline-2d-packer/implementation/
|
||||||
|
fn Point! AtlasBW.place(&atlas, char[] pixels, ushort w, ushort h)
|
||||||
|
{
|
||||||
|
Point p;
|
||||||
|
|
||||||
|
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
|
||||||
|
p = atlas.row;
|
||||||
|
} else {
|
||||||
|
atlas.row.x = 0;
|
||||||
|
atlas.row.y = atlas.row.y + atlas.row_h;
|
||||||
|
atlas.row_h = 0;
|
||||||
|
if (atlas.row.x + w <= atlas.width && atlas.row.y + h <= atlas.height) {
|
||||||
|
p = atlas.row;
|
||||||
|
} else {
|
||||||
|
return UgAtlasError.CANNOT_PLACE?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (usz y = 0; y < h; y++) {
|
||||||
|
for (usz x = 0; x < w; x++) {
|
||||||
|
atlas.buffer[(usz)(p.y+y)*atlas.width + (p.x+x)] = pixels[y*w + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atlas.row.x += w;
|
||||||
|
if (h > atlas.row_h) {
|
||||||
|
atlas.row_h = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn void! Font.load(&font, String path, uint height, float scale = 1)
|
||||||
{
|
{
|
||||||
Font* font = ctx.font;
|
|
||||||
font.table.new_init(capacity: FONT_CACHED);
|
font.table.new_init(capacity: FONT_CACHED);
|
||||||
|
|
||||||
font.size = height*scale;
|
font.size = height*scale;
|
||||||
@ -81,11 +139,21 @@ fn void! Ctx.font_load(&cxt, String path, uint height, float scale = 1)
|
|||||||
font.descender = (float)lmetrics.descender;
|
font.descender = (float)lmetrics.descender;
|
||||||
font.linegap = (float)lmetrics.lineGap;
|
font.linegap = (float)lmetrics.lineGap;
|
||||||
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
|
//io::printfn("ascender:%d, descender:%d, linegap:%d", font.ascender, font.descender, font.linegap);
|
||||||
|
|
||||||
|
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
|
||||||
|
// like the letter 'A'
|
||||||
|
font.atlas = mem::new_array(AtlasBW, 1);
|
||||||
|
ushort size = (ushort)font.size*256;
|
||||||
|
font.atlas[0].new(size, size)!;
|
||||||
|
|
||||||
|
// preallocate the ASCII range
|
||||||
|
// for (char c = ' '; c < '~'; c++) {
|
||||||
|
// font.get_glyph((Codepoint)c)!;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Glyph*! Ctx.get_glyph(&ctx, Codepoint code, bool* is_new = null)
|
fn Glyph*! Font.get_glyph(&font, Codepoint code, bool* is_new = null)
|
||||||
{
|
{
|
||||||
Font* font = &ctx.font;
|
|
||||||
Glyph*! gp;
|
Glyph*! gp;
|
||||||
gp = font.table.get_ref(code);
|
gp = font.table.get_ref(code);
|
||||||
|
|
||||||
@ -131,12 +199,8 @@ fn Glyph*! Ctx.get_glyph(&ctx, Codepoint code, bool* is_new = null)
|
|||||||
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
|
//io::printfn("code=%c, w=%d, h=%d, ox=%d, oy=%d, adv=%d",
|
||||||
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
|
// glyph.code, glyph.w, glyph.h, glyph.ox, glyph.oy, glyph.adv);
|
||||||
|
|
||||||
// TODO: allocate buffer based on FONT_CACHED and the size of a sample letter
|
Point uv = font.atlas[0].place(pixels, glyph.w, glyph.h)!;
|
||||||
// like the letter 'A'
|
glyph.idx = 0;
|
||||||
ushort size = (ushort)font.size*256;
|
|
||||||
Point uv;
|
|
||||||
Id id = ctx.place_sprite(&uv, ATLAS_ALPHA, pixels, glyph.w, glyph.h, size, size)!;
|
|
||||||
glyph.atlas_id = id;
|
|
||||||
glyph.u = uv.x;
|
glyph.u = uv.x;
|
||||||
glyph.v = uv.y;
|
glyph.v = uv.y;
|
||||||
|
|
||||||
@ -148,8 +212,10 @@ fn Glyph*! Ctx.get_glyph(&ctx, Codepoint code, bool* is_new = null)
|
|||||||
return font.table.get_ref(code);
|
return font.table.get_ref(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: maybe some atlases should be exclusive to fonts
|
|
||||||
fn void Font.free(&font)
|
fn void Font.free(&font)
|
||||||
{
|
{
|
||||||
|
foreach (atlas: font.atlas) {
|
||||||
|
atlas.free();
|
||||||
|
}
|
||||||
schrift::freefont(font.sft.font);
|
schrift::freefont(font.sft.font);
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,20 @@ bitstruct MouseButtons : uint {
|
|||||||
bool btn_5 : 4;
|
bool btn_5 : 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
macro Ctx.mouse_pressed(&ctx) => ctx.input.mouse.updated & ctx.input.mouse.down;
|
macro Ctx.mouse_pressed(&ctx)
|
||||||
macro Ctx.mouse_released(&ctx) => ctx.input.mouse.updated & ~ctx.input.mouse.down;
|
{
|
||||||
macro Ctx.mouse_down(&ctx) => ctx.input.mouse.down;
|
return ctx.input.mouse.updated & ctx.input.mouse.down;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.mouse_released(&ctx)
|
||||||
|
{
|
||||||
|
return ctx.input.mouse.updated & ~ctx.input.mouse.down;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.mouse_down(&ctx)
|
||||||
|
{
|
||||||
|
return ctx.input.mouse.down;
|
||||||
|
}
|
||||||
|
|
||||||
const MouseButtons BTN_NONE = (MouseButtons)0u;
|
const MouseButtons BTN_NONE = (MouseButtons)0u;
|
||||||
const MouseButtons BTN_ANY = (MouseButtons)(uint.max);
|
const MouseButtons BTN_ANY = (MouseButtons)(uint.max);
|
||||||
@ -55,9 +66,20 @@ const MouseButtons BTN_5 = {.btn_5 = true};
|
|||||||
|
|
||||||
// FIXME: hthis compairson could be done with a cast using MouseButtons.inner
|
// FIXME: hthis compairson could be done with a cast using MouseButtons.inner
|
||||||
// property but I could not figure out how
|
// property but I could not figure out how
|
||||||
macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn) => (ctx.mouse_pressed() & btn) != BTN_NONE;
|
macro Ctx.is_mouse_pressed(&ctx, MouseButtons btn)
|
||||||
macro Ctx.is_mouse_released(&ctx, MouseButtons btn) => (ctx.mouse_released() & btn) != BTN_NONE;
|
{
|
||||||
macro Ctx.is_mouse_down(&ctx, MouseButtons btn) => (ctx.mouse_down() & btn) != BTN_NONE;
|
return (ctx.mouse_pressed() & btn) != BTN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.is_mouse_released(&ctx, MouseButtons btn)
|
||||||
|
{
|
||||||
|
return (ctx.mouse_released() & btn) != BTN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro Ctx.is_mouse_down(&ctx, MouseButtons btn)
|
||||||
|
{
|
||||||
|
return (ctx.mouse_down() & btn) != BTN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
macro ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
|
macro ElemEvents Ctx.get_elem_events(&ctx, Elem *elem)
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,7 @@ fn Rect! Ctx.get_text_bounds(&ctx, String text, bool* update_atlas)
|
|||||||
off += x;
|
off += x;
|
||||||
bool n;
|
bool n;
|
||||||
if (!ascii::is_cntrl((char)cp)) {
|
if (!ascii::is_cntrl((char)cp)) {
|
||||||
gp = ctx.get_glyph(cp, &n)!;
|
gp = ctx.font.get_glyph(cp, &n)!;
|
||||||
line_len += gp.adv;
|
line_len += gp.adv;
|
||||||
if (n) { *update_atlas = true; }
|
if (n) { *update_atlas = true; }
|
||||||
} else if (cp == '\n'){
|
} else if (cp == '\n'){
|
||||||
|
Loading…
x
Reference in New Issue
Block a user