Compare commits

..

No commits in common. 'b02c1deb79ced20138f60dd58a919b1dd367ce89' and 'f267c12e5cf4e6e0978a343e276f1882298ff544' have entirely different histories.

  1. 12
      src/file.rs
  2. 39
      src/input.rs
  3. 125
      src/main.rs
  4. 156
      src/render.rs
  5. 72
      src/syntax-highlight.rs

@ -1,12 +0,0 @@
//! File management
use std::{fs::File, /* path::Path ,*/ io::Read};
/// Reads the file and turns it into a Vec of u8s
pub fn read_file(file_name: String) -> Result<Vec<u8>, String> {
let mut file_content = Vec::new();
let mut file = File::open(&file_name).expect("Unable to open file");
file.read_to_end(&mut file_content).expect("Unable to read");
Ok(file_content)
}

@ -1,6 +1,5 @@
//use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
/*
mod keybinds { mod keybinds {
pub(super) enum Clipboard { pub(super) enum Clipboard {
CutSelection, CutSelection,
@ -43,41 +42,7 @@ mod keybinds {
WholeLines, WholeLines,
} }
} }
*/
pub struct ModifierKeys { fn input(buffer: &str, modifiers: crate::ModifierKeys, key: Keycode) {
pub alt: bool,
pub ctrl: bool,
pub shift: bool,
}
impl ModifierKeys {
pub fn new() -> ModifierKeys {
ModifierKeys { alt: false, ctrl: false, shift: false }
}
}
pub fn delete(buffer: &mut String, cursor_position: usize, modifiers: &ModifierKeys) {
if modifiers.ctrl {
loop {
let buffer_chars: Vec<char> = buffer.chars()
.collect();
if cursor_position == buffer.len() {
break
}
if !buffer_chars[cursor_position].is_whitespace() {
buffer.remove(cursor_position);
} else {
break
}
}
} else {
buffer.remove(cursor_position);
}
}
/*
fn process_input(buffer: &str, modifiers: ModifierKeys, key: Keycode) {
} }
*/

@ -1,14 +1,13 @@
extern crate sdl2; extern crate sdl2;
use clipboard::{ClipboardContext, ClipboardProvider}; use clipboard::{ClipboardProvider, ClipboardContext};
use sdl2::{ use sdl2::{
event::{Event, WindowEvent}, event::{Event, WindowEvent},
keyboard::Keycode, keyboard::Keycode,
}; };
mod file;
mod input;
mod render; mod render;
mod input;
static SCREEN_WIDTH: u32 = 1280; static SCREEN_WIDTH: u32 = 1280;
static SCREEN_HEIGHT: u32 = 720; static SCREEN_HEIGHT: u32 = 720;
@ -20,6 +19,17 @@ static REFRESH_RATE: u32 = 50;
static UNDO_TIME: u32 = 250; static UNDO_TIME: u32 = 250;
static UNDO_TIME_COUNT: u32 = (REFRESH_RATE as f32 * (UNDO_TIME as f32 / 1000.0)) as u32; static UNDO_TIME_COUNT: u32 = (REFRESH_RATE as f32 * (UNDO_TIME as f32 / 1000.0)) as u32;
struct ModifierKeys {
alt: bool,
ctrl: bool,
shift: bool,
}
// struct EditorGraphics {
// canvas: Canvas<Window>,
// glyph_atlas: GlyphAtlas,
// }
pub fn main() -> Result<(), String> { pub fn main() -> Result<(), String> {
// Initialize clipboard // Initialize clipboard
let mut clipboard_context: ClipboardContext = ClipboardProvider::new().unwrap(); let mut clipboard_context: ClipboardContext = ClipboardProvider::new().unwrap();
@ -47,12 +57,12 @@ pub fn main() -> Result<(), String> {
let mut undo_timer: u32 = UNDO_TIME_COUNT; let mut undo_timer: u32 = UNDO_TIME_COUNT;
// Initialize input values // Initialize input values
let mut modifier_keys = input::ModifierKeys::new(); let mut modifier_keys = ModifierKeys {alt: false, ctrl: false, shift: false};
let mut cursor_position = 0; let mut cursor_position = 0;
let mut selection_anchor: Option<usize> = None; let mut selection_anchor: Option<usize> = None;
// Initialize graphics data and values // Initialize graphics data and values
let glyph_atlas = render::generate_glyph_data()?; let glyph_atlas = render::generate_glyph_data();
// Easier way to please the borrow checker // Easier way to please the borrow checker
macro_rules! draw { macro_rules! draw {
@ -76,39 +86,57 @@ pub fn main() -> Result<(), String> {
// Event::KeyDown { keycode: Some(Keycode::Escape), .. } | // Event::KeyDown { keycode: Some(Keycode::Escape), .. } |
Event::Quit { .. } => break 'mainloop, Event::Quit { .. } => break 'mainloop,
Event::KeyUp { keycode, .. } => match keycode { Event::KeyUp { keycode, .. } => {
Some(Keycode::LAlt) | Some(Keycode::RAlt) => modifier_keys.alt = false, match keycode{
Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => modifier_keys.ctrl = false, Some(Keycode::LAlt) | Some(Keycode::RAlt) => {
Some(Keycode::LShift) | Some(Keycode::RShift) => modifier_keys.shift = false, modifier_keys.alt = false
Some(Keycode::LGui) | Some(Keycode::RGui) => break, },
Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => {
modifier_keys.ctrl = false
},
Some(Keycode::LShift) | Some(Keycode::RShift) => {
modifier_keys.shift = false
},
Some(Keycode::LGui) | Some(Keycode::RGui) => {
break
},
_ => (), _ => (),
}
}, },
Event::KeyDown { keycode, .. } => { Event::KeyDown { keycode, .. } => {
match keycode { match keycode {
Some(Keycode::LAlt) | Some(Keycode::RAlt) => modifier_keys.alt = true, Some(Keycode::LAlt) | Some(Keycode::RAlt) => {
Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => modifier_keys.ctrl = true, modifier_keys.alt = true
Some(Keycode::LShift) | Some(Keycode::RShift) => modifier_keys.shift = true, },
Some(Keycode::LGui) | Some(Keycode::RGui) => break, Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => {
modifier_keys.ctrl = true
},
Some(Keycode::LShift) | Some(Keycode::RShift) => {
modifier_keys.shift = true
},
Some(Keycode::LGui) | Some(Keycode::RGui) => {
break
},
_ => (),
};
match (modifier_keys.shift, modifier_keys.ctrl, modifier_keys.alt) {
// All modifiers up
(false, false, false) => {
match keycode {
// DELETE key // DELETE key
Some(Keycode::Delete) => { Some(Keycode::Delete) => {
if buffer.len() > 0 && cursor_position < buffer.len() { if buffer.len() > 0 && cursor_position < buffer.len() {
undo_timer = 0; undo_timer = 0;
selection_anchor = None; selection_anchor = None;
input::delete(&mut buffer, cursor_position, &modifier_keys);
buffer.remove(cursor_position);
draw!(); draw!();
} }
} },
_ => (),
};
match (modifier_keys.shift, modifier_keys.ctrl, modifier_keys.alt) {
// All modifiers up
(false, false, false) => {
match keycode {
// ENTER key // ENTER key
Some(Keycode::Return) => { Some(Keycode::Return) => {
undo_timer = 0; undo_timer = 0;
@ -119,7 +147,7 @@ pub fn main() -> Result<(), String> {
cursor_position += 1; cursor_position += 1;
draw!(); draw!();
} },
// HOME key // HOME key
Some(Keycode::Home) => { Some(Keycode::Home) => {
@ -128,7 +156,7 @@ pub fn main() -> Result<(), String> {
cursor_position = 0; cursor_position = 0;
draw!(); draw!();
} },
// END key // END key
Some(Keycode::End) => { Some(Keycode::End) => {
@ -137,17 +165,17 @@ pub fn main() -> Result<(), String> {
cursor_position = buffer.len(); cursor_position = buffer.len();
draw!(); draw!();
} },
// Left/Back arrow // Left/Back arrow
Some(Keycode::Left) => { Some(Keycode::Left) => {
selection_anchor = None; selection_anchor = None;
cursor_position = cursor_position = usize::checked_sub(cursor_position, 1)
usize::checked_sub(cursor_position, 1).unwrap_or(0); .unwrap_or(0);
draw!(); draw!();
} },
// Right/Forward arrow // Right/Forward arrow
Some(Keycode::Right) => { Some(Keycode::Right) => {
@ -156,7 +184,7 @@ pub fn main() -> Result<(), String> {
cursor_position = (cursor_position + 1).min(buffer.len()); cursor_position = (cursor_position + 1).min(buffer.len());
draw!(); draw!();
} },
// BACKSPACE key // BACKSPACE key
// Character backspace // Character backspace
@ -170,11 +198,11 @@ pub fn main() -> Result<(), String> {
draw!(); draw!();
} }
} },
_ => (), _ => (),
} }
} },
// CTRL down // CTRL down
(false, true, false) => { (false, true, false) => {
@ -185,7 +213,7 @@ pub fn main() -> Result<(), String> {
cursor_position = 0; cursor_position = 0;
draw!() draw!()
} },
// Undo // Undo
Some(Keycode::Z) => { Some(Keycode::Z) => {
@ -197,7 +225,7 @@ pub fn main() -> Result<(), String> {
draw!() draw!()
} }
} },
// TODO: Cut // TODO: Cut
Some(Keycode::X) => println!("Cut"), Some(Keycode::X) => println!("Cut"),
@ -206,7 +234,7 @@ pub fn main() -> Result<(), String> {
// TODO: Use selection // TODO: Use selection
Some(Keycode::C) => { Some(Keycode::C) => {
clipboard_context.set_contents(buffer.clone()).unwrap() clipboard_context.set_contents(buffer.clone()).unwrap()
} },
// Paste // Paste
Some(Keycode::V) => { Some(Keycode::V) => {
@ -218,7 +246,7 @@ pub fn main() -> Result<(), String> {
undo_timer = UNDO_TIME_COUNT; undo_timer = UNDO_TIME_COUNT;
} }
draw!() draw!()
} },
// BACKSPACE key // BACKSPACE key
// Word backspace // Word backspace
@ -228,11 +256,11 @@ pub fn main() -> Result<(), String> {
undo_timer = 0; undo_timer = 0;
selection_anchor = None; selection_anchor = None;
let buffer_chars: Vec<char> = buffer.chars().collect(); let buffer_chars: Vec<char> = buffer.chars()
while !(buffer_chars[cursor_position - 1] == ' ' .collect();
|| buffer_chars[cursor_position - 1] == '\n') while !(buffer_chars[cursor_position - 1] == ' ' ||
&& cursor_position > 1 buffer_chars[cursor_position - 1] == '\n') &&
{ cursor_position > 1 {
buffer.remove(cursor_position - 1); buffer.remove(cursor_position - 1);
cursor_position -= 1; cursor_position -= 1;
} }
@ -241,13 +269,14 @@ pub fn main() -> Result<(), String> {
draw!() draw!()
} }
} },
_ => (), _ => (),
} }
} },
// SHIFT + CTRL down // SHIFT + CTRL down
(true, true, false) => match keycode { (true, true, false) => {
match keycode {
Some(Keycode::Z) => { Some(Keycode::Z) => {
if undo_position < undo_history.len() { if undo_position < undo_history.len() {
undo_position += 1; undo_position += 1;
@ -257,17 +286,18 @@ pub fn main() -> Result<(), String> {
draw!(); draw!();
} }
} },
Some(Keycode::X) => println!("Cut line(s)"), Some(Keycode::X) => println!("Cut line(s)"),
Some(Keycode::C) => println!("Copy line(s)"), Some(Keycode::C) => println!("Copy line(s)"),
_ => (), _ => (),
}
}, },
_ => (), _ => (),
} }
} },
// Process user input // Process user input
Event::TextInput { text, .. } => { Event::TextInput { text, .. } => {
@ -279,7 +309,7 @@ pub fn main() -> Result<(), String> {
cursor_position += 1; cursor_position += 1;
draw!(); draw!();
} },
_ => {} _ => {}
} }
@ -301,7 +331,6 @@ pub fn main() -> Result<(), String> {
std::thread::sleep(std::time::Duration::new(0, 1_000_000_000 / REFRESH_RATE)); std::thread::sleep(std::time::Duration::new(0, 1_000_000_000 / REFRESH_RATE));
} }
format!("{selection_anchor:?}");
println!("{buffer}"); println!("{buffer}");
Ok(()) Ok(())
} }

@ -11,7 +11,7 @@ use sdl2::{
video::Window, video::Window,
}; };
use crate::file; use std::{fs::File, path::Path, io::Read};
type Glyph = Vec<Point>; type Glyph = Vec<Point>;
@ -31,68 +31,28 @@ pub struct GlyphAtlas {
metrics: GlyphMetrics, metrics: GlyphMetrics,
} }
struct ColorRGB { /// Reads the file and turns it into a Vec of u8s
red: u8, fn read_file(file_name: String) -> Vec<u8> {
green: u8, let path = Path::new(&file_name);
blue: u8,
}
impl ColorRGB { if !path.exists() {
fn new(red: u8, green: u8, blue: u8) -> ColorRGB { return String::from("Not Found!").into();
ColorRGB { red, green, blue }
}
} }
#[allow(dead_code)] let mut file_content = Vec::new();
enum Colors { let mut file = File::open(&file_name).expect("Unable to open file");
Background, file.read_to_end(&mut file_content).expect("Unable to read");
Foreground,
Error,
Warning,
FindBg,
StdFunction,
Comment,
Keyword,
Number,
Operator,
Preprocessor,
SelectionBg,
SelectionFg,
String,
}
impl Colors { file_content
fn color(&self) -> ColorRGB {
match self {
Colors::Background => ColorRGB::new(32, 32, 32),
Colors::Foreground => ColorRGB::new(255, 255, 255),
Colors::Error => ColorRGB::new(255, 0, 0),
Colors::Warning => ColorRGB::new(255, 170, 0),
Colors::FindBg => ColorRGB::new(246, 185, 63),
Colors::StdFunction => ColorRGB::new(170, 255, 255),
Colors::Comment => ColorRGB::new(0, 255, 0),
Colors::Keyword => ColorRGB::new(0, 170, 255),
Colors::Number => ColorRGB::new(85, 255, 255),
Colors::Operator => ColorRGB::new(255, 85, 0),
Colors::Preprocessor => ColorRGB::new(127, 0, 0),
Colors::SelectionBg => ColorRGB::new(110, 161, 241),
Colors::SelectionFg => ColorRGB::new(255, 255, 255),
Colors::String => ColorRGB::new(255, 85, 255),
}
}
} }
pub fn generate_glyph_data() -> Result<GlyphAtlas, String> { pub fn generate_glyph_data() -> GlyphAtlas {
// Retrieve font data from file // Retrieve font data from file
// TODO: Get crate path instead of working directory path
let file_path = String::from("./fonts/Terminus14x8.data"); let file_path = String::from("./fonts/Terminus14x8.data");
let contents = file::read_file(file_path)?; let contents = read_file(file_path);
// Get glyph metrics // Get glyph metrics
let glyph_metrics = GlyphMetrics { let glyph_metrics = GlyphMetrics { width: 8, height: 16 };
width: 8,
height: 16,
};
let glyph_width = glyph_metrics.width; let glyph_width = glyph_metrics.width;
// Get width of image for proper positioning of pixels // Get width of image for proper positioning of pixels
@ -100,9 +60,9 @@ pub fn generate_glyph_data() -> Result<GlyphAtlas, String> {
let width_right_byte = contents[1]; let width_right_byte = contents[1];
let width_bytes = [width_left_byte, width_right_byte]; let width_bytes = [width_left_byte, width_right_byte];
let width = u16::from_be_bytes(width_bytes); let width = u16::from_be_bytes(width_bytes);
println!("Left Byte: {width_left_byte}, Right Byte: {width_right_byte}, Byte Pair: {width}"); // println!("Left Byte: {width_left_byte}, Right Byte: {width_right_byte}, Byte Pair: {width}");
let pruned_glyph_table = &contents[width as usize + 2..]; let gtable_prune = &contents[width as usize + 2 ..];
// Generate the glyph atlas // Generate the glyph atlas
let mut glyph_atlas: Vec<Glyph> = vec![]; let mut glyph_atlas: Vec<Glyph> = vec![];
@ -118,38 +78,30 @@ pub fn generate_glyph_data() -> Result<GlyphAtlas, String> {
let offset = glyph * glyph_width as u16; let offset = glyph * glyph_width as u16;
let position = (x + multiplier + offset) as usize; let position = (x + multiplier + offset) as usize;
if pruned_glyph_table[position] == 1 { if gtable_prune[position] == 1 {
new_glyph.push(Point::new(x as i32, y as i32)); new_glyph.push(Point::new(x as i32, y as i32));
} }
} }
glyph_atlas.push(new_glyph); glyph_atlas.push(new_glyph);
} }
Ok(GlyphAtlas { GlyphAtlas { glyphs: glyph_atlas, metrics: glyph_metrics }
glyphs: glyph_atlas,
metrics: glyph_metrics,
})
} }
/// Method for generating points to render, using given string /// Method for generating points to render, using given string
fn draw_text( fn draw_text(glyph_atlas: &GlyphAtlas, content: &str, offset: Point) -> Vec<Point> {
glyph_atlas: &GlyphAtlas,
window_size: (u32, u32),
content: &str,
offset: Point,
) -> Vec<Point> {
let mut points: Vec<Point> = vec![]; let mut points: Vec<Point> = vec![];
let glyph_width = glyph_atlas.metrics.width; let glyph_width = glyph_atlas.metrics.width;
let glyph_height = glyph_atlas.metrics.height; let glyph_height = glyph_atlas.metrics.height;
let content_lines = content.split('\n'); let lines = content.split('\n');
for (y, line) in content_lines.enumerate() { for (y, chars) in lines.enumerate() {
for (x, character) in line.chars().enumerate() { for (x, chara) in chars.chars().enumerate() {
let index; let index;
if character.is_lowercase() { if chara.is_lowercase() {
index = character.to_ascii_lowercase() as usize; index = chara.to_ascii_lowercase() as usize;
} else { } else {
index = character.to_ascii_uppercase() as usize; index = chara.to_ascii_uppercase() as usize;
} }
for pixel in &glyph_atlas.glyphs[index - 32] { for pixel in &glyph_atlas.glyphs[index - 32] {
@ -158,28 +110,16 @@ fn draw_text(
let positioned_pixel = Point::new( let positioned_pixel = Point::new(
pixel.x + x_glyph as i32 + offset.x, pixel.x + x_glyph as i32 + offset.x,
pixel.y + y_glyph as i32 + offset.y, pixel.y + y_glyph as i32 + offset.y
); );
points.push(positioned_pixel); points.push(positioned_pixel);
} }
if x >= (window_size.0 as usize / glyph_width) - 3 {
break;
}
}
if y >= (window_size.1 as usize / glyph_height) - 3 {
break;
} }
} }
points points
} }
pub fn draw_cursor( pub fn draw_cursor(glyph_atlas: &GlyphAtlas, mut cursor_position: usize, content: &str, offset: Point) -> (Point, Point) {
glyph_atlas: &GlyphAtlas,
mut cursor_position: usize,
content: &str,
offset: Point,
) -> (Point, Point) {
let glyph_width = glyph_atlas.metrics.width; let glyph_width = glyph_atlas.metrics.width;
let glyph_height = glyph_atlas.metrics.height; let glyph_height = glyph_atlas.metrics.height;
@ -187,68 +127,60 @@ pub fn draw_cursor(
let mut y = 0; let mut y = 0;
if cursor_position > 0 { if cursor_position > 0 {
cursor_position = cursor_position.checked_sub(1).unwrap_or(0); cursor_position = cursor_position.checked_sub(1).unwrap_or(0);
for (idx, character) in content.chars().enumerate() { for (idx, chara) in content.chars().enumerate() {
x += 1; x += 1;
if character == '\n' { if chara == '\n' {
x = 0; x = 0;
y += 1; y += 1;
} }
if idx == cursor_position { if idx == cursor_position {
let point_a = Point::new( let point_a = Point::new((x * glyph_width) as i32 + offset.x,
(x * glyph_width) as i32 + offset.x, (y * glyph_height) as i32 + offset.y
(y * glyph_height) as i32 + offset.y, );
let point_b = Point::new(point_a.x,
point_a.y + glyph_height as i32
); );
let point_b = Point::new(point_a.x, point_a.y + glyph_height as i32); return (point_a, point_b)
return (point_a, point_b);
} }
} }
} }
( (Point::new(offset.x, offset.y), Point::new(offset.x, offset.y + glyph_height as i32))
Point::new(offset.x, offset.y),
Point::new(offset.x, offset.y + glyph_height as i32),
)
} }
/// Draw all contents to the window /// Draw all contents to the window
pub fn draw_everything( pub fn draw_everything(canvas: &mut Canvas<Window>,
canvas: &mut Canvas<Window>,
glyph_atlas: &GlyphAtlas, glyph_atlas: &GlyphAtlas,
buffer: &str, buffer: &str,
cursor_position: usize, cursor_position: usize) -> Result<(), String> {
) -> Result<(), String> {
// Quick initialization // Quick initialization
let window_size = canvas.output_size()?; let window_size = canvas.output_size()?;
let text_offset = Point::new(10, 10); let text_offset = Point::new(10, 10);
let bg_color = Colors::Background.color();
let fg_color = Colors::Foreground.color();
// Draw background // Draw background
canvas.set_draw_color(Color::RGB(bg_color.red, bg_color.green, bg_color.blue)); canvas.set_draw_color(Color::RGB(32, 32, 32));
canvas.clear(); canvas.clear();
// Draw text // Draw text
canvas.set_draw_color(Color::RGB(fg_color.red, fg_color.green, fg_color.blue)); canvas.set_draw_color(Color::RGB(240, 240, 240));
let fb_text = draw_text(&glyph_atlas, window_size, buffer, text_offset); let fb_text = draw_text(&glyph_atlas, buffer, text_offset);
canvas.draw_points(&fb_text[..])?; canvas.draw_points(&fb_text[..])?;
// Draw info // Draw info
let status = buffer.len().to_formatted_string(&Locale::en); let status = buffer.len().to_formatted_string(&Locale::en);
let status_position = Point::new( let status_position = Point::new(
text_offset.x, text_offset.x,
window_size.1 as i32 - glyph_atlas.metrics.height as i32 * 2, window_size.1 as i32 - glyph_atlas.metrics.height as i32 * 2
); );
canvas.set_draw_color(Color::RGB(16, 64, 64)); canvas.set_draw_color(Color::RGB(16, 64, 64));
canvas.fill_rect(Rect::new( canvas.fill_rect(Rect::new(0,
0,
status_position.y - 5, status_position.y - 5,
window_size.0, window_size.0,
glyph_atlas.metrics.height as u32 + 10, glyph_atlas.metrics.height as u32 + 10
))?; ))?;
canvas.set_draw_color(Color::RGB(127, 240, 240)); canvas.set_draw_color(Color::RGB(127, 240, 240));
let status_bar = draw_text(&glyph_atlas, window_size, &status, status_position); let status_bar = draw_text(&glyph_atlas, &status, status_position);
canvas.draw_points(&status_bar[..])?; canvas.draw_points(&status_bar[..])?;
// Draw cursor // Draw cursor

@ -1,72 +0,0 @@
/*
pub enum Token {
ILLEGAL,
EOF,
IDENT(Vec<char>),
INT(Vec<char>),
ASSIGN(char),
PLUS(char),
COMMA(char),
SEMICOLON(char),
LPAREN(char),
RPAREN(char),
LBRACE(char),
RBRACE(char),
FUNCTION,
LET,
TRUE,
FALSE,
IF,
ELSE,
RETURN,
MINUS(char),
BANG(char),
ASTERISK(char),
SLASH(char),
LT(char),
GT(char)
}
*/
fn find_keyword(input: &str) {
match input {
"async" => {},
"await" => {},
"as" => {},
"break" => {},
"const" => {},
"continue" => {},
"crate" => {},
"dyn" => {},
"else" => {},
"enum" => {},
"extern" => {},
"false" => {},
"fn" => {},
"for" => {},
"if" => {},
"impl" => {},
"in" => {},
"let" => {},
"loop" => {},
"match" => {},
"mod" => {},
"move" => {},
"mut" => {},
"pub" => {},
"ref" => {},
"return" => {},
"self" => {},
"Self" => {},
"static" => {},
"struct" => {},
"super" => {},
"trait" => {},
"true" => {},
"type" => {},
"unsafe" => {},
"use" => {},
"where" => {},
"while" => {},
}
}
Loading…
Cancel
Save