Added font renderer and terminal emulator
This commit is contained in:
parent
bc4ec556e2
commit
dd4fda27bb
22 changed files with 1921 additions and 5151 deletions
|
@ -2,6 +2,7 @@
|
|||
|
||||
default = 0
|
||||
timeout = 30
|
||||
ui = text
|
||||
|
||||
[AurixOS]
|
||||
protocol = aurix
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,5 +0,0 @@
|
|||
The fonts included in this archive are released under a “no rights reserved” Creative Commons Zero license. Please do not ask permission to do anything with these fonts. Whatever you want to do with this font, the answer will be yes. Please read about the CC0 Public Domain license before contacting me.
|
||||
|
||||
https://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
To the extent possible under law, Raymond Larabie has waived all copyright and related or neighboring rights to the fonts in this archive. This work is published from: Japan.
|
BIN
boot/base/fonts/u_vga16/u_vga16.sfn
Normal file
BIN
boot/base/fonts/u_vga16/u_vga16.sfn
Normal file
Binary file not shown.
BIN
boot/base/fonts/vera/Vera.sfn
Normal file
BIN
boot/base/fonts/vera/Vera.sfn
Normal file
Binary file not shown.
|
@ -42,6 +42,7 @@ char *config_paths[] = {
|
|||
struct axboot_cfg cfg = {
|
||||
.default_entry = DEFAULT_ENTRY,
|
||||
.timeout = DEFAULT_TIMEOUT,
|
||||
.ui_mode = UI_TEXT,
|
||||
|
||||
//.entry_count = 0
|
||||
.entry_count = 2
|
||||
|
@ -103,4 +104,9 @@ int config_get_entry_count()
|
|||
struct axboot_entry *config_get_entries()
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
int config_get_ui_mode()
|
||||
{
|
||||
return cfg.ui_mode;
|
||||
}
|
|
@ -38,9 +38,9 @@ void axboot_init()
|
|||
|
||||
//config_init();
|
||||
|
||||
//ui_init();
|
||||
ui_init();
|
||||
|
||||
//debug("axboot_init(): Returned from main menu, something went wrong. Halting!");
|
||||
debug("axboot_init(): Returned from main menu, something went wrong. Halting!");
|
||||
//UNREACHABLE();
|
||||
|
||||
// just boot aurixos for now
|
||||
|
|
|
@ -62,10 +62,7 @@ void debug(const char *fmt, ...)
|
|||
uart_sendstr(buf);
|
||||
}
|
||||
|
||||
void snprintf(char *buf, size_t size, const char *fmt, ...)
|
||||
void snprintf(char *buf, size_t size, const char *fmt, va_list args)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
npf_vsnprintf(buf, size, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,62 +17,75 @@
|
|||
/* SOFTWARE. */
|
||||
/*********************************************************************************/
|
||||
|
||||
// TODO: Remove this if statement once I fix stb_truetype compilation
|
||||
#if 0
|
||||
|
||||
#include <mm/mman.h>
|
||||
#include <arch/lib/math.h>
|
||||
#include <lib/string.h>
|
||||
#include <lib/assert.h>
|
||||
#include <vfs/vfs.h>
|
||||
#define FONT_IMPLEMENTATION
|
||||
#include <ui/ui.h>
|
||||
#include <ui/font.h>
|
||||
#include <print.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
__attribute__((used)) static volatile int _fltused = 0;
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
|
||||
#define STBTT_ifloor(x) _ifloor(x)
|
||||
#define STBTT_iceil(x) _iceil(x)
|
||||
#define STBTT_sqrt(x) _sqrt(x)
|
||||
#define STBTT_pow(x, y) _pow(x, y)
|
||||
#define STBTT_fmod(x, y) _fmod(x, y)
|
||||
#define STBTT_cos(x) _cos(x)
|
||||
#define STBTT_acos(x) _acos(x)
|
||||
#define STBTT_fabs(x) __builtin_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x, u) ((void)(u), mem_alloc(x))
|
||||
#define STBTT_free(x, u) ((void)(u), mem_free(x))
|
||||
|
||||
#define STBTT_assert(x) assert(x, #x)
|
||||
#define STBTT_strlen(x) strlen(x)
|
||||
|
||||
#define STBTT_memcpy memcpy
|
||||
#define STBTT_memset memset
|
||||
|
||||
#include "stb_truetype.h"
|
||||
|
||||
unsigned char *font_buf = NULL;
|
||||
stbtt_fontinfo font_info;
|
||||
float font_scale;
|
||||
int font_ascent, font_descent;
|
||||
int font_linegap;
|
||||
int font_size;
|
||||
|
||||
void font_init(char *font_path, int initial_size)
|
||||
bool font_init(struct ui_context *ctx, char *font_path, int size)
|
||||
{
|
||||
vfs_read(font_path, &font_buf);
|
||||
vfs_read(font_path, &(ctx->font_file));
|
||||
if (!ctx->font_file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int ssfn_status;
|
||||
|
||||
ssfn_status = ssfn_load(&(ctx->font), (void *)(ctx->font_file));
|
||||
if (ssfn_status != SSFN_OK) {
|
||||
debug("font_init(): SSFN failed to load font: %s!\n", ssfn_error(ssfn_status));
|
||||
goto error;
|
||||
}
|
||||
|
||||
ssfn_status = ssfn_select(&(ctx->font), SSFN_FAMILY_ANY, NULL, SSFN_STYLE_REGULAR, size);
|
||||
if (ssfn_status != SSFN_OK) {
|
||||
debug("font_init(): SSFN failed to select font: %s!\n", ssfn_error(ssfn_status));
|
||||
goto error;
|
||||
}
|
||||
|
||||
// initialize terminal
|
||||
ctx->terminal.font_size = size;
|
||||
ctx->terminal.cx = 0;
|
||||
ctx->terminal.cy = size;
|
||||
|
||||
return true;
|
||||
|
||||
error:
|
||||
mem_free(ctx->font_file);
|
||||
return false;
|
||||
}
|
||||
|
||||
void font_write(struct ui_context *ctx, char *s, uint32_t cx, uint32_t cy)
|
||||
{
|
||||
ctx->font_buf.x = cx;
|
||||
ctx->font_buf.y = cy;
|
||||
ssfn_render(&(ctx->font), &(ctx->font_buf), s);
|
||||
}
|
||||
|
||||
void font_free(struct ui_context *ctx)
|
||||
{
|
||||
ssfn_free(&(ctx->font));
|
||||
mem_free(ctx->font_file);
|
||||
}
|
||||
|
||||
/*
|
||||
void font_ttf_init(char *font_path, int initial_size)
|
||||
{
|
||||
vfs_read(font_path, (char **)&font_buf);
|
||||
if (!font_buf) {
|
||||
debug("Font not loaded, returning...\n");
|
||||
return;
|
||||
}
|
||||
|
||||
font_size = initial_size;
|
||||
|
||||
stbtt_InitFont(&font_info, &font_buf, 0);
|
||||
font_scale = stbtt_ScaleForPixelHeight(&font_info, font_size);
|
||||
stbtt_GetFontVMetrics(&font_info, &font_ascent, &font_descent, &font_linegap);
|
||||
font_ascent *= font_scale;
|
||||
font_descent *= font_scale;
|
||||
}
|
||||
|
||||
#endif
|
||||
void font_psf2_init()
|
||||
{
|
||||
}
|
||||
*/
|
File diff suppressed because it is too large
Load diff
86
boot/common/ui/terminal.c
Normal file
86
boot/common/ui/terminal.c
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*********************************************************************************/
|
||||
/* Module Name: terminal.c */
|
||||
/* Project: AurixOS */
|
||||
/* */
|
||||
/* Copyright (c) 2024-2025 Jozef Nagy */
|
||||
/* */
|
||||
/* This source is subject to the MIT License. */
|
||||
/* See License.txt in the root of this repository. */
|
||||
/* All other rights reserved. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR */
|
||||
/* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, */
|
||||
/* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE */
|
||||
/* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER */
|
||||
/* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, */
|
||||
/* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE */
|
||||
/* SOFTWARE. */
|
||||
/*********************************************************************************/
|
||||
|
||||
#include <ui/ui.h>
|
||||
#include <ui/terminal.h>
|
||||
#include <axboot.h>
|
||||
#include <print.h>
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#define HORIZONTAL_TAB_WIDTH 8
|
||||
|
||||
void terminal_print(struct ui_context *ctx, char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
char buf[4096] = {0};
|
||||
char *s = (char *)&buf;
|
||||
|
||||
va_start(args, fmt);
|
||||
snprintf((char *)&buf, sizeof(buf), fmt, args);
|
||||
va_end(args);
|
||||
|
||||
while (*s) {
|
||||
switch (*s) {
|
||||
case '\t': {
|
||||
// horizontal tab - 4 spaces
|
||||
int w, h;
|
||||
ssfn_bbox(&(ctx->font), " ", &w, &h, NULL, NULL);
|
||||
if (ctx->terminal.cx >= ctx->fb_modes[ctx->current_mode].width - (w * HORIZONTAL_TAB_WIDTH)) {
|
||||
for (int i = 1; i <= HORIZONTAL_TAB_WIDTH; i++) {
|
||||
if (ctx->terminal.cx >= ctx->fb_modes[ctx->current_mode].width - (w * i)) {
|
||||
ctx->terminal.cx = ((HORIZONTAL_TAB_WIDTH * (w + 1)) - (w * i));
|
||||
ctx->terminal.cy += h;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx->terminal.cx += w * HORIZONTAL_TAB_WIDTH;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '\n': {
|
||||
// newline
|
||||
ctx->terminal.cx = 0;
|
||||
ctx->terminal.cy += ctx->terminal.font_size;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// printable character
|
||||
const char str[2] = {*s, 0};
|
||||
int w, h;
|
||||
ssfn_bbox(&(ctx->font), (char *)&str, &w, &h, NULL, NULL);
|
||||
if (ctx->terminal.cx + w >= ctx->fb_modes[ctx->current_mode].width) {
|
||||
ctx->terminal.cx = 0;
|
||||
ctx->terminal.cy += h;
|
||||
}
|
||||
font_write(ctx, (char *)&str, ctx->terminal.cx, ctx->terminal.cy);
|
||||
ctx->terminal.cx += w;
|
||||
break;
|
||||
}
|
||||
}
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
void terminal_setcur(struct ui_context *ui, uint32_t x, uint32_t y)
|
||||
{
|
||||
ui->terminal.cx = x;
|
||||
ui->terminal.cy = y;
|
||||
}
|
|
@ -17,40 +17,110 @@
|
|||
/* SOFTWARE. */
|
||||
/*********************************************************************************/
|
||||
|
||||
#include <config/config.h>
|
||||
#include <lib/string.h>
|
||||
#include <ui/framebuffer.h>
|
||||
#include <ui/mouse.h>
|
||||
#include <ui/font.h>
|
||||
#include <ui/ui.h>
|
||||
#include <config/config.h>
|
||||
#include <axboot.h>
|
||||
|
||||
#include <print.h>
|
||||
#include <stdint.h>
|
||||
|
||||
bool gui_init(struct ui_context *ctx)
|
||||
{
|
||||
if (!font_init(ctx, "\\AxBoot\\fonts\\vera\\Vera.sfn", 16)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool tui_init(struct ui_context *ctx)
|
||||
{
|
||||
if (!font_init(ctx, "\\AxBoot\\fonts\\u_vga16\\u_vga16.sfn", 16)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char *top_string = BOOTLOADER_NAME_STR " ver. " BOOTLOADER_VERSION_STR;
|
||||
int top_string_w;
|
||||
ssfn_bbox(&(ctx->font), top_string, &top_string_w, NULL, NULL, NULL);
|
||||
|
||||
terminal_setcur(ctx, ctx->fb_modes[ctx->current_mode].width / 2 - (top_string_w / 2), ctx->fb_modes[ctx->current_mode].height / 32);
|
||||
terminal_print(ctx, top_string);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void gui_draw(void *mouse_status, void *event)
|
||||
{
|
||||
(void)mouse_status;
|
||||
(void)event;
|
||||
}
|
||||
|
||||
void tui_draw(void *mouse_status, void *event)
|
||||
{
|
||||
(void)mouse_status;
|
||||
(void)event;
|
||||
}
|
||||
|
||||
void ui_init()
|
||||
{
|
||||
struct ui_context ctx;
|
||||
struct ui_context ctx = {0};
|
||||
|
||||
if (!get_framebuffer(&ctx.fb_addr, &ctx.fb_modes, &ctx.total_modes, &ctx.current_mode)) {
|
||||
debug("Failed to acquire a framebuffer!\n");
|
||||
debug("ui_init(): Failed to acquire a framebuffer!\n");
|
||||
while (1);
|
||||
}
|
||||
|
||||
ctx.ui = config_get_ui_mode();
|
||||
|
||||
debug("Dumping framebuffer information\n");
|
||||
debug("--------------------------------\n");
|
||||
debug("Address: 0x%llx\n", ctx.fb_addr);
|
||||
|
||||
for (int i = 0; i < ctx.total_modes; i++) {
|
||||
debug("\nMode %u:%s\n", i, (i == ctx.current_mode) ? " (current)" : "");
|
||||
debug("Resolution: %ux%u\n", ctx.fb_modes[i].width, ctx.fb_modes[i].height);
|
||||
debug("Bits Per Pixel: %u\n", ctx.fb_modes[i].bpp);
|
||||
debug("Pitch: %u\n", ctx.fb_modes[i].pitch);
|
||||
debug("Mode %u:%s | ", i, (i == ctx.current_mode) ? " (current)" : "");
|
||||
debug("Resolution: %ux%u | ", ctx.fb_modes[i].width, ctx.fb_modes[i].height);
|
||||
debug("Bits Per Pixel: %u | ", ctx.fb_modes[i].bpp);
|
||||
debug("Pitch: %u | ", ctx.fb_modes[i].pitch);
|
||||
debug("Format: %s\n", ctx.fb_modes[i].format == FB_RGBA ? "RGBA" : "BGRA");
|
||||
}
|
||||
|
||||
//font_init("\\AxBoot\\fonts\\DreamOrphans.ttf", 20);
|
||||
ctx.font_buf.ptr = (uint8_t *)ctx.fb_addr;
|
||||
ctx.font_buf.w = ctx.fb_modes[ctx.current_mode].width;
|
||||
ctx.font_buf.h = ctx.fb_modes[ctx.current_mode].height;
|
||||
ctx.font_buf.p = ctx.fb_modes[ctx.current_mode].pitch;
|
||||
ctx.font_buf.x = 0;
|
||||
ctx.font_buf.y = 0;
|
||||
ctx.font_buf.fg = 0xFFFFFFFF;
|
||||
|
||||
//while (1) {
|
||||
void (*ui_callback)(void*,void*) = NULL;
|
||||
|
||||
switch (ctx.ui) {
|
||||
case UI_MODERN: {
|
||||
if (!gui_init(&ctx)) {
|
||||
debug("ui_init(): Failed to initialize modern UI, booting default selection...\n");
|
||||
break;
|
||||
}
|
||||
ui_callback = gui_draw;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
case UI_TEXT: {
|
||||
if (!tui_init(&ctx)) {
|
||||
debug("ui_init(): Failed to initialize text UI, booting default selection...\n");
|
||||
break;
|
||||
}
|
||||
ui_callback = tui_draw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (1) {
|
||||
ui_callback(NULL, NULL);
|
||||
//get_mouse(&m_x, &m_y, &m_but);
|
||||
//debug("Mouse X = %u | Mouse Y = %u\n", m_x, m_y);
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,16 @@
|
|||
#ifndef _CONFIG_CONFIG_H
|
||||
#define _CONFIG_CONFIG_H
|
||||
|
||||
enum {
|
||||
UI_TEXT = 0,
|
||||
UI_MODERN = 1
|
||||
};
|
||||
|
||||
struct axboot_cfg {
|
||||
// overridable stuff
|
||||
int default_entry;
|
||||
int timeout;
|
||||
int ui_mode;
|
||||
|
||||
int entry_count;
|
||||
};
|
||||
|
@ -37,6 +43,10 @@ struct axboot_entry {
|
|||
|
||||
void config_init(void);
|
||||
|
||||
int config_get_timeout();
|
||||
int config_get_default();
|
||||
int config_get_entry_count();
|
||||
struct axboot_entry *config_get_entries();
|
||||
int config_get_ui_mode();
|
||||
|
||||
#endif /* _CONFIG_CONFIG_H */
|
||||
|
|
|
@ -30,6 +30,6 @@ void debug(const char *fmt, ...);
|
|||
|
||||
void printstr(const char *str);
|
||||
|
||||
void snprintf(char *buf, size_t size, const char *fmt, ...);
|
||||
void snprintf(char *buf, size_t size, const char *fmt, va_list args);
|
||||
|
||||
#endif /* _PRINT_H */
|
|
@ -20,6 +20,14 @@
|
|||
#ifndef _UI_FONT_H
|
||||
#define _UI_FONT_H
|
||||
|
||||
void font_init(char *font_path, int initial_size);
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct ui_context;
|
||||
|
||||
bool font_init(struct ui_context *ctx, char *font_path, int size);
|
||||
void font_free(struct ui_context *ctx);
|
||||
|
||||
void font_write(struct ui_context *ctx, char *s, uint32_t cx, uint32_t cy);
|
||||
|
||||
#endif /* _UI_FONT_H */
|
||||
|
|
1604
boot/include/ui/ssfn.h
Normal file
1604
boot/include/ui/ssfn.h
Normal file
File diff suppressed because it is too large
Load diff
37
boot/include/ui/terminal.h
Normal file
37
boot/include/ui/terminal.h
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*********************************************************************************/
|
||||
/* Module Name: terminal.h */
|
||||
/* Project: AurixOS */
|
||||
/* */
|
||||
/* Copyright (c) 2024-2025 Jozef Nagy */
|
||||
/* */
|
||||
/* This source is subject to the MIT License. */
|
||||
/* See License.txt in the root of this repository. */
|
||||
/* All other rights reserved. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR */
|
||||
/* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, */
|
||||
/* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE */
|
||||
/* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER */
|
||||
/* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, */
|
||||
/* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE */
|
||||
/* SOFTWARE. */
|
||||
/*********************************************************************************/
|
||||
|
||||
#ifndef _UI_TERMINAL_H
|
||||
#define _UI_TERMINAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct terminal {
|
||||
uint32_t cx;
|
||||
uint32_t cy;
|
||||
|
||||
int font_size;
|
||||
};
|
||||
|
||||
struct ui_context;
|
||||
|
||||
void terminal_print(struct ui_context *ctx, char *fmt, ...);
|
||||
void terminal_setcur(struct ui_context *ui, uint32_t x, uint32_t y);
|
||||
|
||||
#endif /* _UI_TERMINAL_H */
|
|
@ -21,6 +21,21 @@
|
|||
#define _UI_UI_H
|
||||
|
||||
#include <ui/framebuffer.h>
|
||||
#include <ui/terminal.h>
|
||||
#include <ui/font.h>
|
||||
|
||||
// this is so hacky... but it works
|
||||
#ifdef FONT_IMPLEMENTATION
|
||||
#include <lib/string.h>
|
||||
#include <mm/mman.h>
|
||||
|
||||
#define SSFN_memcmp memcmp
|
||||
#define SSFN_memset memset
|
||||
#define SSFN_realloc mem_realloc
|
||||
#define SSFN_free mem_free
|
||||
#define SSFN_IMPLEMENTATION
|
||||
#endif
|
||||
#include <ui/ssfn.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
|
@ -29,6 +44,13 @@ struct ui_context {
|
|||
struct fb_mode *fb_modes;
|
||||
int total_modes;
|
||||
int current_mode;
|
||||
int ui;
|
||||
|
||||
struct terminal terminal;
|
||||
|
||||
ssfn_t font;
|
||||
ssfn_buf_t font_buf;
|
||||
char *font_file;
|
||||
};
|
||||
|
||||
void ui_init();
|
||||
|
|
|
@ -57,7 +57,7 @@ EFI_STATUS uefi_entry(EFI_HANDLE ImageHandle,
|
|||
// disable UEFI watchdog
|
||||
Status = gSystemTable->BootServices->SetWatchdogTimer(0, 0, 0, NULL);
|
||||
if (EFI_ERROR(Status)) {
|
||||
debug("Couldn't disable UEFI watchdog: %s (%x)\n", efi_status_to_str(Status), Status);
|
||||
debug("uefi_entry(): Couldn't disable UEFI watchdog: %s (%x)\n", efi_status_to_str(Status), Status);
|
||||
}
|
||||
|
||||
// load that mouse up
|
||||
|
@ -75,7 +75,7 @@ EFI_STATUS uefi_entry(EFI_HANDLE ImageHandle,
|
|||
continue;
|
||||
}
|
||||
|
||||
debug("Found SPP with ResX=%u, ResY=%u\n", spp[i]->Mode->ResolutionX, spp[i]->Mode->ResolutionY);
|
||||
debug("uefi_entry(): Found SPP with ResX=%u, ResY=%u\n", spp[i]->Mode->ResolutionX, spp[i]->Mode->ResolutionY);
|
||||
if (spp[i]->Reset(spp[i], EFI_TRUE) == EFI_DEVICE_ERROR) {
|
||||
debug("uefi_entry(): Failed to reset device\n");
|
||||
continue;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue