mirror of
https://github.com/Astatin3/IntroToWebAuthoring.git
synced 2026-06-09 00:28:00 -06:00
Get constraint layout work with xml
This commit is contained in:
+28
-2
@@ -1,2 +1,28 @@
|
|||||||
<ColoredRectView r="20" g="40" b="60" />
|
<ConstraintLayout>
|
||||||
<!-- </ColoredRectView> -->
|
<BoxView margin="10" align_top_to_top="parent" align_left_to_left="parent">
|
||||||
|
<ColoredRectView r="123" g="12" b="34" />
|
||||||
|
</BoxView>
|
||||||
|
|
||||||
|
<BoxView
|
||||||
|
margin_left="10"
|
||||||
|
margin_right="10"
|
||||||
|
align_top_to_top="parent"
|
||||||
|
align_left_to_left="parent"
|
||||||
|
align_bottom_to_bottom="parent"
|
||||||
|
align_right_to_right="parent"
|
||||||
|
width="400"
|
||||||
|
height="parent"
|
||||||
|
>
|
||||||
|
<ColoredRectView r="30" g="30" b="30" />
|
||||||
|
</BoxView>
|
||||||
|
|
||||||
|
<BoxView
|
||||||
|
margin="10"
|
||||||
|
align_top_to_top="parent"
|
||||||
|
align_left_to_right="2"
|
||||||
|
align_bottom_to_bottom="parent"
|
||||||
|
width="200"
|
||||||
|
>
|
||||||
|
<ColoredRectView r="123" g="125" b="34" />
|
||||||
|
</BoxView>
|
||||||
|
</ConstraintLayout>
|
||||||
|
|||||||
+3
-1
@@ -5,9 +5,10 @@ pub use cursors::{Cursor, set_cursor};
|
|||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
log,
|
||||||
parser::{self, TEST_XML},
|
parser::{self, TEST_XML},
|
||||||
render::Renderer,
|
render::Renderer,
|
||||||
views::{View, default_view},
|
views::View,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -38,6 +39,7 @@ impl App {
|
|||||||
let (width, height) = (renderer.actual_width, renderer.actual_height);
|
let (width, height) = (renderer.actual_width, renderer.actual_height);
|
||||||
|
|
||||||
let root_view = parser::parse(TEST_XML);
|
let root_view = parser::parse(TEST_XML);
|
||||||
|
log!("root: {:?}", root_view);
|
||||||
|
|
||||||
let mut this = App {
|
let mut this = App {
|
||||||
root_view: Some(root_view),
|
root_view: Some(root_view),
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
parser::Tag,
|
|
||||||
views::{ColorRectView, ConstraintLayout, View},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn interpret_tag(tag: Tag) -> Box<dyn View> {
|
|
||||||
match tag.name.as_str() {
|
|
||||||
"ColoredRectView" => Box::new(ColorRectView::from_tag(&tag.attributes)),
|
|
||||||
"ConstraintLayout" => Box::new(ConstraintLayout::from_tag(&tag.attributes)),
|
|
||||||
|
|
||||||
_ => panic!("Unknown tag: {}", tag.name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+51
-8
@@ -1,20 +1,63 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::views::View;
|
use crate::{
|
||||||
|
log,
|
||||||
|
views::{Bounds, BoxView, ColorRectView, ConstraintLayout, View},
|
||||||
|
};
|
||||||
|
|
||||||
mod interpreter;
|
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
||||||
pub const TEST_XML: &'static str = include_str!("../../pages/main.xml");
|
pub const TEST_XML: &'static str = include_str!("../../pages/main.xml");
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Tag {
|
pub struct Tag {
|
||||||
name: String,
|
pub name: String,
|
||||||
children: Vec<Tag>,
|
pub children: Vec<Tag>,
|
||||||
attributes: HashMap<String, String>,
|
pub attributes: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
pub fn parse(&self) -> Box<dyn View> {
|
||||||
|
match self.name.as_str() {
|
||||||
|
"ColoredRectView" => ColorRectView::from_tag(&self.attributes, &self.children),
|
||||||
|
"ConstraintLayout" => ConstraintLayout::from_tag(&self.attributes, &self.children),
|
||||||
|
"BoxView" => BoxView::from_tag(&self.attributes, &self.children),
|
||||||
|
|
||||||
|
_ => panic!("Unknown tag: {}", self.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn parse_bounds(attributes: &HashMap<String, String>) -> (Option<Bounds>, Option<Bounds>) {
|
||||||
|
let (mut width, mut height) = (None, None);
|
||||||
|
|
||||||
|
let parse_bound = |str: &str| -> Bounds {
|
||||||
|
match str {
|
||||||
|
"parent" => Bounds::MatchParent,
|
||||||
|
_ => {
|
||||||
|
let bound = if let Ok(int) = str.parse::<i32>() {
|
||||||
|
int as f32
|
||||||
|
} else if let Ok(float) = str.parse::<f32>() {
|
||||||
|
float
|
||||||
|
} else {
|
||||||
|
panic!("Invalid String");
|
||||||
|
};
|
||||||
|
|
||||||
|
Bounds::Pixels(bound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (key, value) in attributes {
|
||||||
|
match key.as_str() {
|
||||||
|
"width" => width = Some(parse_bound(value)),
|
||||||
|
"height" => height = Some(parse_bound(value)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(width, height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(xml: &str) -> Box<dyn View> {
|
pub fn parse(xml: &str) -> Box<dyn View> {
|
||||||
let tag = parser::parse_xml(xml);
|
parser::parse_xml(xml).parse()
|
||||||
interpreter::interpret_tag(tag)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,15 @@ fn parse_tag(event: BytesStart<'_>, reader: &mut Reader<&[u8]>) -> Tag {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
match reader.read_event_into(&mut buf) {
|
match reader.read_event_into(&mut buf) {
|
||||||
Err(e) => panic!("Error at position {}: {:?}", reader.error_position(), e),
|
Err(e) => {
|
||||||
|
log!("Error at position {}: {:?}", reader.error_position(), e);
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
// exits the loop when reaching end of file
|
// exits the loop when reaching end of file
|
||||||
Ok(Event::Eof) => panic!("Unexpected end of file"),
|
Ok(Event::Eof) => {
|
||||||
|
log!("Unexpected end of file");
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Event::Start(e)) => {
|
Ok(Event::Start(e)) => {
|
||||||
children.push(parse_tag(e, reader));
|
children.push(parse_tag(e, reader));
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
|
|
||||||
use wasm_bindgen::{Clamped, prelude::*};
|
use wasm_bindgen::{Clamped, prelude::*};
|
||||||
use web_sys::ImageData;
|
use web_sys::ImageData;
|
||||||
|
|
||||||
use crate::render::{
|
use crate::render::{RESOLUTION, buffer::ImgBuffer, calc_resolution, rand::Rnd};
|
||||||
RESOLUTION,
|
|
||||||
buffer::{Bitmap, ImgBuffer},
|
|
||||||
calc_resolution,
|
|
||||||
rand::Rnd,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
use std::any::TypeId;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
log,
|
||||||
|
parser::Tag,
|
||||||
|
render::Renderer,
|
||||||
|
views::{Bounds, Constraint, View},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BoxView {
|
||||||
|
view: Box<dyn View>,
|
||||||
|
bounds: (Option<Bounds>, Option<Bounds>),
|
||||||
|
constraints: (
|
||||||
|
Option<Constraint>,
|
||||||
|
Option<Constraint>,
|
||||||
|
Option<Constraint>,
|
||||||
|
Option<Constraint>,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoxView {
|
||||||
|
pub fn new(view: Box<dyn View>) -> Self {
|
||||||
|
Self {
|
||||||
|
view,
|
||||||
|
bounds: (None, None),
|
||||||
|
constraints: (None, None, None, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_bounds(&mut self, width: Option<Bounds>, height: Option<Bounds>) {
|
||||||
|
self.bounds = (width, height);
|
||||||
|
}
|
||||||
|
pub fn set_constraints(
|
||||||
|
&mut self,
|
||||||
|
top: Option<Constraint>,
|
||||||
|
left: Option<Constraint>,
|
||||||
|
right: Option<Constraint>,
|
||||||
|
bottom: Option<Constraint>,
|
||||||
|
) {
|
||||||
|
self.constraints = (top, left, right, bottom)
|
||||||
|
}
|
||||||
|
pub fn bounds(&self, _ph: f32, _pw: f32) -> (Bounds, Bounds) {
|
||||||
|
(
|
||||||
|
*self.bounds.0.as_ref().unwrap_or(&Bounds::Pixels(100.)),
|
||||||
|
*self.bounds.1.as_ref().unwrap_or(&Bounds::Pixels(100.)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn get_constraints(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
&Option<Constraint>,
|
||||||
|
&Option<Constraint>,
|
||||||
|
&Option<Constraint>,
|
||||||
|
&Option<Constraint>,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
&self.constraints.0,
|
||||||
|
&self.constraints.1,
|
||||||
|
&self.constraints.2,
|
||||||
|
&self.constraints.3,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for BoxView {
|
||||||
|
fn draw(&mut self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32) {
|
||||||
|
self.view.draw(renderer, x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize(&mut self, x: f32, y: f32, w: f32, h: f32) {
|
||||||
|
self.view.resize(x, y, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_tag(
|
||||||
|
attributes: &std::collections::HashMap<String, String>,
|
||||||
|
children: &Vec<Tag>,
|
||||||
|
) -> Box<dyn View> {
|
||||||
|
assert!(children.len() == 1, "BoxView must have exactly one child");
|
||||||
|
|
||||||
|
let (mut margin_top, mut margin_left, mut margin_right, mut margin_bottom) =
|
||||||
|
(None, None, None, None);
|
||||||
|
|
||||||
|
let (mut align_top, mut align_left, mut align_right, mut align_bottom) =
|
||||||
|
(None, None, None, None);
|
||||||
|
|
||||||
|
let parse_float = |var: &mut Option<f32>, value: &str| {
|
||||||
|
if let Ok(float) = value.parse::<f32>() {
|
||||||
|
*var = Some(float);
|
||||||
|
} else if let Ok(int) = value.parse::<i32>() {
|
||||||
|
let float = int as f32;
|
||||||
|
*var = Some(float);
|
||||||
|
} else {
|
||||||
|
panic!("Could not parse margin!")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let parse_align_refrence = |value: &String| -> usize {
|
||||||
|
match value.as_str() {
|
||||||
|
"parent" => 0,
|
||||||
|
_ => {
|
||||||
|
if let Ok(parent) = value.parse::<usize>() {
|
||||||
|
if parent >= children.len() {
|
||||||
|
// panic!("Child {} does not exist!", parent);
|
||||||
|
}
|
||||||
|
parent
|
||||||
|
} else {
|
||||||
|
panic!("Could not parse refrence");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (key, value) in attributes {
|
||||||
|
match key.as_str() {
|
||||||
|
// Set margin on all sides
|
||||||
|
"margin" => {
|
||||||
|
if let Ok(float) = value.parse::<f32>() {
|
||||||
|
(margin_top, margin_left, margin_right, margin_bottom) =
|
||||||
|
(Some(float), Some(float), Some(float), Some(float));
|
||||||
|
} else if let Ok(int) = value.parse::<i32>() {
|
||||||
|
let float = int as f32;
|
||||||
|
(margin_top, margin_left, margin_right, margin_bottom) =
|
||||||
|
(Some(float), Some(float), Some(float), Some(float));
|
||||||
|
} else {
|
||||||
|
panic!("Could not parse margin!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Margin for specific directions
|
||||||
|
"margin_top" => parse_float(&mut margin_top, value),
|
||||||
|
"margin_left" => parse_float(&mut margin_left, value),
|
||||||
|
"margin_right" => parse_float(&mut margin_right, value),
|
||||||
|
"margin_bottom" => parse_float(&mut margin_bottom, value),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in attributes {
|
||||||
|
match key.as_str() {
|
||||||
|
// Alignment
|
||||||
|
"align_top_to_top" => {
|
||||||
|
align_top = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::TopParent(margin_top.unwrap_or(0.)),
|
||||||
|
n => Constraint::Top(margin_top.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"align_top_to_bottom" => {
|
||||||
|
align_top = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::BottomParent(margin_top.unwrap_or(0.)),
|
||||||
|
n => Constraint::Bottom(margin_top.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
"align_left_to_left" => {
|
||||||
|
align_left = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::LeftParent(margin_left.unwrap_or(0.)),
|
||||||
|
n => Constraint::Left(margin_left.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"align_left_to_right" => {
|
||||||
|
align_left = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::RightParent(margin_left.unwrap_or(0.)),
|
||||||
|
n => Constraint::Right(margin_left.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
"align_right_to_right" => {
|
||||||
|
align_right = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::RightParent(margin_right.unwrap_or(0.)),
|
||||||
|
n => Constraint::Right(margin_right.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"align_right_to_left" => {
|
||||||
|
align_right = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::LeftParent(margin_right.unwrap_or(0.)),
|
||||||
|
n => Constraint::Left(margin_right.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
"align_bottom_to_bottom" => {
|
||||||
|
align_bottom = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::BottomParent(margin_bottom.unwrap_or(0.)),
|
||||||
|
n => Constraint::Bottom(margin_bottom.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"align_bottom_to_top" => {
|
||||||
|
align_bottom = Some(match parse_align_refrence(value) {
|
||||||
|
0 => Constraint::TopParent(margin_bottom.unwrap_or(0.)),
|
||||||
|
n => Constraint::Top(margin_bottom.unwrap_or(0.), n - 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut b = Self::new(children[0].parse());
|
||||||
|
|
||||||
|
b.constraints = (align_top, align_left, align_right, align_bottom);
|
||||||
|
b.bounds = Tag::parse_bounds(attributes);
|
||||||
|
|
||||||
|
Box::new(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,41 +1,17 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
parser::Tag,
|
||||||
render::Renderer,
|
render::Renderer,
|
||||||
views::{
|
views::{Bounds, Constraint, View},
|
||||||
Bounds, LayoutView, View,
|
|
||||||
constraint_layout::{Constraint, ConstraintView},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ColorRectView {
|
pub struct ColorRectView {
|
||||||
color: (u8, u8, u8),
|
color: (u8, u8, u8),
|
||||||
bounds: (Option<Bounds>, Option<Bounds>),
|
|
||||||
constraints: (
|
|
||||||
Option<Constraint>,
|
|
||||||
Option<Constraint>,
|
|
||||||
Option<Constraint>,
|
|
||||||
Option<Constraint>,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ColorRectView {
|
impl ColorRectView {
|
||||||
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
Self {
|
Self { color: (r, g, b) }
|
||||||
color: (r, g, b),
|
|
||||||
bounds: (None, None),
|
|
||||||
constraints: (None, None, None, None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set_bounds(&mut self, width: Option<Bounds>, height: Option<Bounds>) {
|
|
||||||
self.bounds = (width, height);
|
|
||||||
}
|
|
||||||
pub fn set_constraints(
|
|
||||||
&mut self,
|
|
||||||
top: Option<Constraint>,
|
|
||||||
left: Option<Constraint>,
|
|
||||||
right: Option<Constraint>,
|
|
||||||
bottom: Option<Constraint>,
|
|
||||||
) {
|
|
||||||
self.constraints = (top, left, right, bottom)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +23,18 @@ impl View for ColorRectView {
|
|||||||
|
|
||||||
fn resize(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
|
fn resize(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
|
||||||
|
|
||||||
fn from_tag(attributes: &std::collections::HashMap<String, String>) -> Self
|
fn from_tag(
|
||||||
|
attributes: &std::collections::HashMap<String, String>,
|
||||||
|
children: &Vec<Tag>,
|
||||||
|
) -> Box<dyn View>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
|
assert!(
|
||||||
|
children.is_empty(),
|
||||||
|
"ColorRectView does not support children"
|
||||||
|
);
|
||||||
|
|
||||||
let r = attributes
|
let r = attributes
|
||||||
.get("r")
|
.get("r")
|
||||||
.unwrap_or(&"0".to_string())
|
.unwrap_or(&"0".to_string())
|
||||||
@@ -67,33 +51,10 @@ impl View for ColorRectView {
|
|||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Self::new(r, g, b)
|
Box::new(Self::new(r, g, b))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutView for ColorRectView {
|
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
|
||||||
fn bounds(&self, _ph: f32, _pw: f32) -> (Bounds, Bounds) {
|
self
|
||||||
(
|
|
||||||
*self.bounds.0.as_ref().unwrap_or(&Bounds::Pixels(100.)),
|
|
||||||
*self.bounds.1.as_ref().unwrap_or(&Bounds::Pixels(100.)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConstraintView for ColorRectView {
|
|
||||||
fn get_constraints(
|
|
||||||
&self,
|
|
||||||
) -> (
|
|
||||||
&Option<Constraint>,
|
|
||||||
&Option<Constraint>,
|
|
||||||
&Option<Constraint>,
|
|
||||||
&Option<Constraint>,
|
|
||||||
) {
|
|
||||||
(
|
|
||||||
&self.constraints.0,
|
|
||||||
&self.constraints.1,
|
|
||||||
&self.constraints.2,
|
|
||||||
&self.constraints.3,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,23 @@
|
|||||||
use std::collections::HashMap;
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
use crate::{
|
collections::HashMap,
|
||||||
render::Renderer,
|
|
||||||
views::{LayoutView, View},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum Constraint {
|
use crate::{
|
||||||
Left(f32, usize),
|
log,
|
||||||
Right(f32, usize),
|
parser::Tag,
|
||||||
Top(f32, usize),
|
render::Renderer,
|
||||||
Bottom(f32, usize),
|
views::{Constraint, View, box_view::BoxView},
|
||||||
|
};
|
||||||
LeftParent(f32),
|
|
||||||
RightParent(f32),
|
|
||||||
TopParent(f32),
|
|
||||||
BottomParent(f32),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ConstraintView: LayoutView {
|
|
||||||
fn get_constraints(
|
|
||||||
&self,
|
|
||||||
) -> (
|
|
||||||
&Option<Constraint>,
|
|
||||||
&Option<Constraint>,
|
|
||||||
&Option<Constraint>,
|
|
||||||
&Option<Constraint>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ConstraintLayout {
|
pub struct ConstraintLayout {
|
||||||
pub children: Vec<Box<dyn ConstraintView>>,
|
pub children: Vec<BoxView>,
|
||||||
child_positions: Option<HashMap<usize, Rect>>,
|
child_positions: Option<HashMap<usize, Rect>>,
|
||||||
|
child_order: Option<Vec<usize>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct Rect {
|
struct Rect {
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
@@ -41,7 +26,7 @@ struct Rect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConstraintLayout {
|
impl ConstraintLayout {
|
||||||
pub fn new(children: Vec<Box<dyn ConstraintView>>) -> Self {
|
pub fn new(children: Vec<BoxView>) -> Self {
|
||||||
for child in &children {
|
for child in &children {
|
||||||
let (top, left, right, bottom) = child.get_constraints();
|
let (top, left, right, bottom) = child.get_constraints();
|
||||||
|
|
||||||
@@ -55,10 +40,15 @@ impl ConstraintLayout {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConstraintLayout {
|
let mut this = ConstraintLayout {
|
||||||
children,
|
children,
|
||||||
child_positions: None,
|
child_positions: None,
|
||||||
}
|
child_order: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.recalculate_child_render_order();
|
||||||
|
|
||||||
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recalculate_positions(&mut self, x: f32, y: f32, self_w: f32, self_h: f32) {
|
pub fn recalculate_positions(&mut self, x: f32, y: f32, self_w: f32, self_h: f32) {
|
||||||
@@ -69,19 +59,11 @@ impl ConstraintLayout {
|
|||||||
|
|
||||||
let mut calculated_positions: HashMap<usize, Rect> = HashMap::new();
|
let mut calculated_positions: HashMap<usize, Rect> = HashMap::new();
|
||||||
|
|
||||||
let dependencies = self
|
let child_order = self.child_order.as_ref().unwrap();
|
||||||
.children
|
for idx in child_order {
|
||||||
.iter()
|
let child: &mut BoxView = &mut self.children[*idx];
|
||||||
.map(|c| {
|
|
||||||
let constraints = c.get_constraints();
|
|
||||||
[constraints.0, constraints.1, constraints.2, constraints.3]
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let sorted = topological_sort(&dependencies);
|
log!("{:?}", child.get_constraints());
|
||||||
|
|
||||||
for idx in sorted {
|
|
||||||
let child = &mut self.children[idx];
|
|
||||||
|
|
||||||
let (c_top, c_left, c_right, c_bottom) = child.get_constraints();
|
let (c_top, c_left, c_right, c_bottom) = child.get_constraints();
|
||||||
let (c_width, c_height) = child.bounds(self_w, self_h);
|
let (c_width, c_height) = child.bounds(self_w, self_h);
|
||||||
@@ -152,7 +134,7 @@ impl ConstraintLayout {
|
|||||||
};
|
};
|
||||||
|
|
||||||
calculated_positions.insert(
|
calculated_positions.insert(
|
||||||
idx,
|
*idx,
|
||||||
Rect {
|
Rect {
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
@@ -164,6 +146,19 @@ impl ConstraintLayout {
|
|||||||
|
|
||||||
self.child_positions = Some(calculated_positions);
|
self.child_positions = Some(calculated_positions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn recalculate_child_render_order(&mut self) {
|
||||||
|
let dependencies = self
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
let constraints = c.get_constraints();
|
||||||
|
[constraints.0, constraints.1, constraints.2, constraints.3]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
self.child_order = Some(topological_sort(&dependencies));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for ConstraintLayout {
|
impl View for ConstraintLayout {
|
||||||
@@ -180,11 +175,30 @@ impl View for ConstraintLayout {
|
|||||||
self.recalculate_positions(x, y, w, h);
|
self.recalculate_positions(x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_tag(attributes: &std::collections::HashMap<String, String>) -> Self
|
fn from_tag(
|
||||||
|
attributes: &std::collections::HashMap<String, String>,
|
||||||
|
children: &Vec<Tag>,
|
||||||
|
) -> Box<dyn View>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
todo!()
|
let mut parsed_children = Vec::new();
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
let child = child.parse();
|
||||||
|
|
||||||
|
if let Ok(b) = child.as_any().downcast::<BoxView>() {
|
||||||
|
parsed_children.push(*b);
|
||||||
|
} else {
|
||||||
|
panic!("Constraint Layout must contain only BoxView!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box::new(Self::new(parsed_children))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +233,8 @@ fn dfs(
|
|||||||
result: &mut Vec<usize>,
|
result: &mut Vec<usize>,
|
||||||
) {
|
) {
|
||||||
if states[node] == VisitState::Visiting {
|
if states[node] == VisitState::Visiting {
|
||||||
panic!("Cycle detected in constraint graph at node {}", node);
|
log!("Cycle detected in constraint graph at node {}", node);
|
||||||
|
panic!();
|
||||||
}
|
}
|
||||||
|
|
||||||
if states[node] == VisitState::Visited {
|
if states[node] == VisitState::Visited {
|
||||||
|
|||||||
+40
-28
@@ -1,27 +1,30 @@
|
|||||||
use std::collections::HashMap;
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
use crate::{
|
collections::HashMap,
|
||||||
render::Renderer,
|
fmt::Debug,
|
||||||
views::constraint_layout::{Constraint, ConstraintView},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{parser::Tag, render::Renderer};
|
||||||
|
|
||||||
|
mod box_view;
|
||||||
mod color_rect_view;
|
mod color_rect_view;
|
||||||
mod constraint_layout;
|
mod constraint_layout;
|
||||||
mod text_view;
|
mod text_view;
|
||||||
mod vertical_layout;
|
mod vertical_layout;
|
||||||
|
|
||||||
|
pub use box_view::BoxView;
|
||||||
pub use color_rect_view::ColorRectView;
|
pub use color_rect_view::ColorRectView;
|
||||||
pub use constraint_layout::ConstraintLayout;
|
pub use constraint_layout::ConstraintLayout;
|
||||||
pub use text_view::TextView;
|
|
||||||
pub use vertical_layout::VerticalLayout;
|
|
||||||
|
|
||||||
pub trait View {
|
pub trait View: Debug {
|
||||||
fn draw(&mut self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32);
|
fn draw(&mut self, renderer: &mut Renderer, x: f32, y: f32, w: f32, h: f32);
|
||||||
fn resize(&mut self, x: f32, y: f32, w: f32, h: f32);
|
fn resize(&mut self, x: f32, y: f32, w: f32, h: f32);
|
||||||
|
|
||||||
fn from_tag(attributes: &HashMap<String, String>) -> Self
|
fn from_tag(attributes: &HashMap<String, String>, children: &Vec<Tag>) -> Box<dyn View>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
|
fn as_any(self: Box<Self>) -> Box<dyn Any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -30,17 +33,32 @@ pub enum Bounds {
|
|||||||
Pixels(f32),
|
Pixels(f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait LayoutView: View {
|
#[derive(Debug)]
|
||||||
// fn set_bounds(&mut self, width: Option<Bounds>, height: Option<Bounds>);
|
pub enum Constraint {
|
||||||
fn bounds(&self, pw: f32, ph: f32) -> (Bounds, Bounds);
|
Left(f32, usize),
|
||||||
|
Right(f32, usize),
|
||||||
|
Top(f32, usize),
|
||||||
|
Bottom(f32, usize),
|
||||||
|
|
||||||
|
LeftParent(f32),
|
||||||
|
RightParent(f32),
|
||||||
|
TopParent(f32),
|
||||||
|
BottomParent(f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub trait ViewPosition: Drawable {
|
// pub trait ConstraintView: View {
|
||||||
// // fn draw(&self)
|
// fn get_constraints(
|
||||||
|
// &self,
|
||||||
|
// ) -> (
|
||||||
|
// &Option<Constraint>,
|
||||||
|
// &Option<Constraint>,
|
||||||
|
// &Option<Constraint>,
|
||||||
|
// &Option<Constraint>,
|
||||||
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub fn default_view() -> Box<dyn View> {
|
pub fn default_view() -> Box<dyn View> {
|
||||||
let mut a = ColorRectView::new(127, 0, 0);
|
let mut a = BoxView::new(Box::new(ColorRectView::new(127, 0, 0)));
|
||||||
|
|
||||||
let temp_margin = 10.0;
|
let temp_margin = 10.0;
|
||||||
|
|
||||||
@@ -53,7 +71,7 @@ pub fn default_view() -> Box<dyn View> {
|
|||||||
);
|
);
|
||||||
a.set_bounds(Some(Bounds::Pixels(200.)), Some(Bounds::Pixels(100.)));
|
a.set_bounds(Some(Bounds::Pixels(200.)), Some(Bounds::Pixels(100.)));
|
||||||
|
|
||||||
let mut b = ColorRectView::new(0, 127, 0);
|
let mut b = BoxView::new(Box::new(ColorRectView::new(0, 127, 0)));
|
||||||
|
|
||||||
b.set_constraints(
|
b.set_constraints(
|
||||||
Some(Constraint::TopParent(temp_margin)),
|
Some(Constraint::TopParent(temp_margin)),
|
||||||
@@ -62,7 +80,7 @@ pub fn default_view() -> Box<dyn View> {
|
|||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut c = ColorRectView::new(0, 0, 127);
|
let mut c = BoxView::new(Box::new(ColorRectView::new(0, 0, 127)));
|
||||||
|
|
||||||
c.set_constraints(
|
c.set_constraints(
|
||||||
None,
|
None,
|
||||||
@@ -71,7 +89,7 @@ pub fn default_view() -> Box<dyn View> {
|
|||||||
Some(Constraint::BottomParent(temp_margin)),
|
Some(Constraint::BottomParent(temp_margin)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut d = ColorRectView::new(127, 127, 0);
|
let mut d = BoxView::new(Box::new(ColorRectView::new(127, 127, 0)));
|
||||||
|
|
||||||
d.set_constraints(
|
d.set_constraints(
|
||||||
None,
|
None,
|
||||||
@@ -80,7 +98,7 @@ pub fn default_view() -> Box<dyn View> {
|
|||||||
Some(Constraint::BottomParent(temp_margin)),
|
Some(Constraint::BottomParent(temp_margin)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut center = ColorRectView::new(127, 127, 0);
|
let mut center = BoxView::new(Box::new(ColorRectView::new(127, 127, 0)));
|
||||||
|
|
||||||
center.set_constraints(
|
center.set_constraints(
|
||||||
Some(Constraint::TopParent(temp_margin)),
|
Some(Constraint::TopParent(temp_margin)),
|
||||||
@@ -91,7 +109,7 @@ pub fn default_view() -> Box<dyn View> {
|
|||||||
|
|
||||||
center.set_bounds(Some(Bounds::Pixels(350.)), Some(Bounds::Pixels(350.)));
|
center.set_bounds(Some(Bounds::Pixels(350.)), Some(Bounds::Pixels(350.)));
|
||||||
|
|
||||||
let mut center_top = ColorRectView::new(127, 0, 127);
|
let mut center_top = BoxView::new(Box::new(ColorRectView::new(127, 0, 127)));
|
||||||
|
|
||||||
center_top.set_constraints(
|
center_top.set_constraints(
|
||||||
Some(Constraint::Bottom(0., 4)),
|
Some(Constraint::Bottom(0., 4)),
|
||||||
@@ -103,7 +121,7 @@ pub fn default_view() -> Box<dyn View> {
|
|||||||
Some(Constraint::Top(0., 4)),
|
Some(Constraint::Top(0., 4)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut inside_top = ColorRectView::new(127, 0, 127);
|
let mut inside_top = BoxView::new(Box::new(ColorRectView::new(127, 0, 127)));
|
||||||
|
|
||||||
inside_top.set_constraints(
|
inside_top.set_constraints(
|
||||||
Some(Constraint::Bottom(0., 4)),
|
Some(Constraint::Bottom(0., 4)),
|
||||||
@@ -116,13 +134,7 @@ pub fn default_view() -> Box<dyn View> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Box::new(ConstraintLayout::new(vec![
|
Box::new(ConstraintLayout::new(vec![
|
||||||
Box::new(a),
|
a, b, c, d, center, center_top, inside_top,
|
||||||
Box::new(b),
|
|
||||||
Box::new(c),
|
|
||||||
Box::new(d),
|
|
||||||
Box::new(center),
|
|
||||||
Box::new(center_top),
|
|
||||||
Box::new(inside_top),
|
|
||||||
]))
|
]))
|
||||||
|
|
||||||
// Box::new(VerticalLayout::new(vec![
|
// Box::new(VerticalLayout::new(vec![
|
||||||
|
|||||||
+34
-16
@@ -1,9 +1,12 @@
|
|||||||
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
|
use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fonts::{FONTS, FontHandle},
|
fonts::{FONTS, FontHandle},
|
||||||
|
parser::Tag,
|
||||||
render::{Renderer, buffer::Bitmap},
|
render::{Renderer, buffer::Bitmap},
|
||||||
views::{Bounds, LayoutView, View},
|
views::{Bounds, View},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TextView {
|
pub struct TextView {
|
||||||
@@ -11,7 +14,6 @@ pub struct TextView {
|
|||||||
text: String,
|
text: String,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
font: FontHandle,
|
font: FontHandle,
|
||||||
bounds: (Option<Bounds>, Option<Bounds>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextView {
|
impl TextView {
|
||||||
@@ -27,7 +29,6 @@ impl TextView {
|
|||||||
text,
|
text,
|
||||||
scale: 20.,
|
scale: 20.,
|
||||||
font: FontHandle::AtiksonHyperlegibleRegular,
|
font: FontHandle::AtiksonHyperlegibleRegular,
|
||||||
bounds: (None, None),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let font = (s.font as usize).clone();
|
let font = (s.font as usize).clone();
|
||||||
@@ -39,9 +40,6 @@ impl TextView {
|
|||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
pub fn set_bounds(&mut self, width: Option<Bounds>, height: Option<Bounds>) {
|
|
||||||
self.bounds = (width, height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for TextView {
|
impl View for TextView {
|
||||||
@@ -79,24 +77,44 @@ impl View for TextView {
|
|||||||
// todo!()
|
// todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_tag(attributes: &std::collections::HashMap<String, String>) -> Self
|
fn from_tag(
|
||||||
|
attributes: &std::collections::HashMap<String, String>,
|
||||||
|
children: &Vec<Tag>,
|
||||||
|
) -> Box<dyn View>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
|
assert!(children.is_empty(), "TextView does not support children");
|
||||||
todo!()
|
todo!()
|
||||||
|
// Self::new(attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutView for TextView {
|
// impl LayoutView for TextView {
|
||||||
fn bounds(&self, _ph: f32, _pw: f32) -> (Bounds, Bounds) {
|
// fn bounds(&self, _ph: f32, _pw: f32) -> (Bounds, Bounds) {
|
||||||
// let (x, y) = renderer.undistort(x as f32, y as f32);
|
// // let (x, y) = renderer.undistort(x as f32, y as f32);
|
||||||
|
|
||||||
let (mut width, mut height): (usize, usize) = (0, 0);
|
// let (mut width, mut height): (usize, usize) = (0, 0);
|
||||||
for glyph in self.layout.glyphs() {
|
// for glyph in self.layout.glyphs() {
|
||||||
width = width.max(glyph.x as usize + glyph.width);
|
// width = width.max(glyph.x as usize + glyph.width);
|
||||||
height = height.max(glyph.y as usize + glyph.height);
|
// height = height.max(glyph.y as usize + glyph.height);
|
||||||
}
|
// }
|
||||||
|
|
||||||
(Bounds::Pixels(width as f32), Bounds::Pixels(height as f32))
|
// (Bounds::Pixels(width as f32), Bounds::Pixels(height as f32))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl Debug for TextView {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("TextView")
|
||||||
|
// .field("layout", &self.layout)
|
||||||
|
.field("text", &self.text)
|
||||||
|
.field("scale", &self.scale)
|
||||||
|
.field("font", &self.font)
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
use crate::views::{Bounds, LayoutView, View};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
parser::Tag,
|
||||||
|
views::{Bounds, View, box_view::BoxView},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct VerticalLayout {
|
pub struct VerticalLayout {
|
||||||
pub views: Vec<Box<dyn LayoutView>>,
|
pub views: Vec<BoxView>,
|
||||||
pub bounds: (Option<Bounds>, Option<Bounds>),
|
pub bounds: (Option<Bounds>, Option<Bounds>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VerticalLayout {
|
impl VerticalLayout {
|
||||||
pub fn new(views: Vec<Box<dyn LayoutView>>) -> Self {
|
pub fn new(views: Vec<BoxView>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
views,
|
views,
|
||||||
bounds: (None, None),
|
bounds: (None, None),
|
||||||
@@ -42,34 +48,43 @@ impl View for VerticalLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn resize(&mut self, x: f32, y: f32, w: f32, h: f32) {}
|
fn resize(&mut self, x: f32, y: f32, w: f32, h: f32) {}
|
||||||
fn from_tag(attributes: &std::collections::HashMap<String, String>) -> Self
|
|
||||||
|
fn from_tag(attributes: &HashMap<String, String>, children: &Vec<Tag>) -> Box<dyn View>
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
todo!()
|
todo!("");
|
||||||
|
// Box::new(Self {
|
||||||
|
// views: children.iter().map(|tag| tag.parse()).collect(),
|
||||||
|
// bounds: (None, None),
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(self: Box<Self>) -> Box<dyn std::any::Any> {
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LayoutView for VerticalLayout {
|
// impl LayoutView for VerticalLayout {
|
||||||
fn bounds(&self, pw: f32, ph: f32) -> (Bounds, Bounds) {
|
// fn bounds(&self, pw: f32, ph: f32) -> (Bounds, Bounds) {
|
||||||
let (mut maxx, mut totaly): (f32, f32) = (0., 0.);
|
// let (mut maxx, mut totaly): (f32, f32) = (0., 0.);
|
||||||
for view in &self.views {
|
// for view in &self.views {
|
||||||
let (vx, vy) = view.bounds(pw, ph);
|
// let (vx, vy) = view.bounds(pw, ph);
|
||||||
|
|
||||||
let vx = match vx {
|
// let vx = match vx {
|
||||||
super::Bounds::MatchParent => pw,
|
// super::Bounds::MatchParent => pw,
|
||||||
super::Bounds::Pixels(x) => x,
|
// super::Bounds::Pixels(x) => x,
|
||||||
};
|
// };
|
||||||
|
|
||||||
let vy = match vy {
|
// let vy = match vy {
|
||||||
super::Bounds::MatchParent => ph,
|
// super::Bounds::MatchParent => ph,
|
||||||
super::Bounds::Pixels(x) => x,
|
// super::Bounds::Pixels(x) => x,
|
||||||
};
|
// };
|
||||||
|
|
||||||
maxx = maxx.max(vx);
|
// maxx = maxx.max(vx);
|
||||||
totaly += vy;
|
// totaly += vy;
|
||||||
}
|
// }
|
||||||
|
|
||||||
(Bounds::Pixels(maxx), Bounds::Pixels(totaly))
|
// (Bounds::Pixels(maxx), Bounds::Pixels(totaly))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
Reference in New Issue
Block a user