mirror of
https://github.com/griffi-gh/hUI.git
synced 2025-04-17 13:07:19 -05:00
Compare commits
2 commits
6ce17d0625
...
679ee9a2ce
Author | SHA1 | Date | |
---|---|---|---|
|
679ee9a2ce | ||
|
5515cbf84b |
|
@ -22,3 +22,4 @@ rect_packer = "0.2" # TODO: use sth else like `crunch` instead?
|
||||||
hashbrown = "0.15"
|
hashbrown = "0.15"
|
||||||
nohash-hasher = "0.2"
|
nohash-hasher = "0.2"
|
||||||
fontdue = "0.9"
|
fontdue = "0.9"
|
||||||
|
rustc-hash = "2.0"
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
use hui_shared::rect::Rect;
|
||||||
use crate::{paint::buffer::PaintBuffer, PainterInstance};
|
use crate::{paint::buffer::PaintBuffer, PainterInstance};
|
||||||
|
|
||||||
// mod root;
|
// mod root;
|
||||||
|
@ -29,14 +32,16 @@ pub trait PaintCommand {
|
||||||
/// Do not allocate new textures or cache glyphs here, use `pre_paint` instead!\
|
/// Do not allocate new textures or cache glyphs here, use `pre_paint` instead!\
|
||||||
/// (Doing this WILL lead to atlas corruption flicker for a single frame if it's forced to resize!)
|
/// (Doing this WILL lead to atlas corruption flicker for a single frame if it's forced to resize!)
|
||||||
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer);
|
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer);
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Measurable: PaintCommand {
|
/// Hash of the parameters that affect command's appearance
|
||||||
fn size(&self, ctx: &PainterInstance) -> Vec2;
|
///
|
||||||
|
/// Must be unique for each possilbe combination of parameters
|
||||||
|
fn cache_hash(&self) -> u64;
|
||||||
|
|
||||||
|
fn bounds(&self, ctx: &PainterInstance) -> Rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move paint_root to PaintCommand instead of separate trait?
|
// TODO move paint_root to PaintCommand instead of separate trait?
|
||||||
|
|
||||||
pub trait PaintRoot: PaintCommand {
|
pub trait PaintRoot: PaintCommand {
|
||||||
/// Paint the root command, calling `pre_paint` before painting
|
/// Paint the root command, calling `pre_paint` before painting
|
||||||
///
|
///
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
use std::hash::Hasher;
|
||||||
|
|
||||||
|
use hui_shared::rect::Rect;
|
||||||
|
|
||||||
use crate::PainterInstance;
|
use crate::PainterInstance;
|
||||||
|
|
||||||
use super::PaintCommand;
|
use super::PaintCommand;
|
||||||
|
@ -42,4 +46,29 @@ impl PaintCommand for PaintList {
|
||||||
command.paint(ctx, into);
|
command.paint(ctx, into);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cache_hash(&self) -> u64 {
|
||||||
|
let mut hasher = rustc_hash::FxHasher::default();
|
||||||
|
for command in self.commands.iter() {
|
||||||
|
hasher.write_u64(command.cache_hash());
|
||||||
|
}
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds(&self, ctx: &PainterInstance) -> Rect {
|
||||||
|
if self.commands.is_empty() {
|
||||||
|
return Rect::ZERO;
|
||||||
|
}
|
||||||
|
let mut position = glam::Vec2::splat(f32::MAX);
|
||||||
|
let mut size = glam::Vec2::splat(f32::MIN);
|
||||||
|
for command in &self.commands {
|
||||||
|
let bounds = command.bounds(ctx);
|
||||||
|
position = position.min(bounds.position);
|
||||||
|
size = size.max(bounds.size);
|
||||||
|
}
|
||||||
|
Rect {
|
||||||
|
position,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
use std::num::NonZeroU16;
|
use std::{hash::Hasher, num::NonZeroU16};
|
||||||
use glam::{vec2, Vec2};
|
use glam::{vec2, Vec2};
|
||||||
use hui_shared::{color, rect::{Corners, FillColor}};
|
use hui_shared::{color, rect::{Corners, FillColor, Rect}};
|
||||||
use crate::{
|
use crate::{
|
||||||
paint::{
|
paint::{
|
||||||
buffer::{PaintBuffer, Vertex},
|
buffer::{PaintBuffer, Vertex},
|
||||||
command::PaintCommand,
|
command::PaintCommand,
|
||||||
},
|
},
|
||||||
texture::TextureHandle,
|
texture::TextureHandle,
|
||||||
PainterInstance
|
util::{hash_vec2, hash_vec3, hash_vec4},
|
||||||
|
PainterInstance,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Calculate the number of points based on the maximum corner radius
|
/// Calculate the number of points based on the maximum corner radius
|
||||||
|
@ -93,6 +94,11 @@ impl PaintRectangle {
|
||||||
|
|
||||||
impl PaintCommand for PaintRectangle {
|
impl PaintCommand for PaintRectangle {
|
||||||
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer) {
|
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer) {
|
||||||
|
// Offset from (0, 0) to the actual origin
|
||||||
|
// We calculate positions in the range of [0, size] for simplicity
|
||||||
|
// And then subtract this offset to get the actual position of a rectangle centered at (0, 0)
|
||||||
|
// let origin_offset = self.size / 2.;
|
||||||
|
|
||||||
// If texture is set:
|
// If texture is set:
|
||||||
// - Get texture UV
|
// - Get texture UV
|
||||||
// - Map local UVs to texture UV coords
|
// - Map local UVs to texture UV coords
|
||||||
|
@ -165,22 +171,22 @@ impl PaintCommand for PaintRectangle {
|
||||||
]);
|
]);
|
||||||
into.vertices.extend([
|
into.vertices.extend([
|
||||||
Vertex {
|
Vertex {
|
||||||
position: vec2(0., 0.) * self.size,
|
position: vec2(0., 0.) * self.size, // - origin_offset,
|
||||||
uv: uvs.top_left,
|
uv: uvs.top_left,
|
||||||
color: colors.top_left,
|
color: colors.top_left,
|
||||||
},
|
},
|
||||||
Vertex {
|
Vertex {
|
||||||
position: vec2(1., 0.) * self.size,
|
position: vec2(1., 0.) * self.size, // - origin_offset,
|
||||||
uv: uvs.top_right,
|
uv: uvs.top_right,
|
||||||
color: colors.top_right,
|
color: colors.top_right,
|
||||||
},
|
},
|
||||||
Vertex {
|
Vertex {
|
||||||
position: vec2(0., 1.) * self.size,
|
position: vec2(0., 1.) * self.size, // - origin_offset,
|
||||||
uv: uvs.bottom_left,
|
uv: uvs.bottom_left,
|
||||||
color: colors.bottom_left,
|
color: colors.bottom_left,
|
||||||
},
|
},
|
||||||
Vertex {
|
Vertex {
|
||||||
position: vec2(1., 1.) * self.size,
|
position: vec2(1., 1.) * self.size, // - origin_offset,
|
||||||
uv: uvs.bottom_right,
|
uv: uvs.bottom_right,
|
||||||
color: colors.bottom_right,
|
color: colors.bottom_right,
|
||||||
},
|
},
|
||||||
|
@ -207,7 +213,7 @@ impl PaintCommand for PaintRectangle {
|
||||||
uvs.bottom_left * (1. - point_uv.x) * point_uv.y +
|
uvs.bottom_left * (1. - point_uv.x) * point_uv.y +
|
||||||
uvs.top_left * (1. - point_uv.x) * (1. - point_uv.y);
|
uvs.top_left * (1. - point_uv.x) * (1. - point_uv.y);
|
||||||
Vertex {
|
Vertex {
|
||||||
position: point,
|
position: point, // - origin_offset,
|
||||||
color: color_at_point,
|
color: color_at_point,
|
||||||
uv: uv_at_point,
|
uv: uv_at_point,
|
||||||
}
|
}
|
||||||
|
@ -271,7 +277,27 @@ impl PaintCommand for PaintRectangle {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
unimplemented!("Border radius is not supported yet");
|
// unimplemented!("Border radius is not supported yet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bounds(&self, _: &PainterInstance) -> Rect {
|
||||||
|
// Rect {
|
||||||
|
// position: -self.size / 2.,
|
||||||
|
// size: self.size / 2.,
|
||||||
|
// }
|
||||||
|
Rect {
|
||||||
|
position: Vec2::ZERO,
|
||||||
|
size: self.size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_hash(&self) -> u64 {
|
||||||
|
let mut hasher = rustc_hash::FxHasher::default();
|
||||||
|
hash_vec2(&mut hasher, self.size);
|
||||||
|
for corner in self.color.corners() {
|
||||||
|
hash_vec4(&mut hasher, corner);
|
||||||
|
}
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, hash::{Hash, Hasher}};
|
||||||
use fontdue::layout::{CoordinateSystem, Layout};
|
use fontdue::layout::{CoordinateSystem, Layout};
|
||||||
use glam::{vec2, Vec2};
|
use glam::{vec2, Vec2};
|
||||||
|
use hui_shared::rect::Rect;
|
||||||
use crate::{
|
use crate::{
|
||||||
paint::{
|
paint::{
|
||||||
buffer::PaintBuffer,
|
buffer::PaintBuffer,
|
||||||
|
@ -8,7 +9,6 @@ use crate::{
|
||||||
}, text::FontHandle, PainterInstance
|
}, text::FontHandle, PainterInstance
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::Measurable;
|
|
||||||
|
|
||||||
// TODO align, multichunk etc
|
// TODO align, multichunk etc
|
||||||
|
|
||||||
|
@ -75,12 +75,12 @@ impl PaintCommand for PaintText {
|
||||||
// let glyph_raster = ctx.fonts().render_glyph(atlas, font, config);
|
// let glyph_raster = ctx.fonts().render_glyph(atlas, font, config);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
todo!()
|
// todo!()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Measurable for PaintText {
|
// TODO_IMPORTANT text rendering
|
||||||
fn size(&self, ctx: &PainterInstance) -> Vec2 {
|
}
|
||||||
|
|
||||||
|
fn bounds(&self, ctx: &PainterInstance) -> Rect {
|
||||||
let font_array = self.build_font_array(ctx);
|
let font_array = self.build_font_array(ctx);
|
||||||
let layout = self.build_layout(&font_array);
|
let layout = self.build_layout(&font_array);
|
||||||
|
|
||||||
|
@ -92,6 +92,17 @@ impl Measurable for PaintText {
|
||||||
}).unwrap_or(0.);
|
}).unwrap_or(0.);
|
||||||
let height = layout.height();
|
let height = layout.height();
|
||||||
|
|
||||||
vec2(width, height)
|
Rect {
|
||||||
|
position: vec2(0., 0.),
|
||||||
|
size: vec2(width, height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_hash(&self) -> u64 {
|
||||||
|
let mut hasher = rustc_hash::FxHasher::default();
|
||||||
|
self.text.font.hash(&mut hasher);
|
||||||
|
hasher.write_u32(self.text.size.to_bits());
|
||||||
|
hasher.write(self.text.text.as_bytes());
|
||||||
|
hasher.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
use std::hash::Hasher;
|
||||||
|
|
||||||
|
use glam::vec2;
|
||||||
|
use hui_shared::rect::Rect;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PainterInstance,
|
PainterInstance,
|
||||||
paint::{
|
paint::{
|
||||||
|
@ -23,6 +28,11 @@ impl<T: PaintCommand + 'static> PaintCommand for PaintTransform<T> {
|
||||||
// paint children node
|
// paint children node
|
||||||
self.child.paint(ctx, into);
|
self.child.paint(ctx, into);
|
||||||
|
|
||||||
|
if starting_index == into.vertices.len() {
|
||||||
|
// no vertices were added, no need to transform
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut min_point = glam::Vec2::splat(f32::MAX);
|
let mut min_point = glam::Vec2::splat(f32::MAX);
|
||||||
let mut max_point = glam::Vec2::splat(f32::MIN);
|
let mut max_point = glam::Vec2::splat(f32::MIN);
|
||||||
for vtx in &into.vertices[starting_index..] {
|
for vtx in &into.vertices[starting_index..] {
|
||||||
|
@ -40,4 +50,41 @@ impl<T: PaintCommand + 'static> PaintCommand for PaintTransform<T> {
|
||||||
vtx.position += offset;
|
vtx.position += offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cache_hash(&self) -> u64 {
|
||||||
|
let mut hasher = rustc_hash::FxHasher::default();
|
||||||
|
hasher.write_u64(self.child.cache_hash());
|
||||||
|
self.transform.to_cols_array().iter().for_each(|v| {
|
||||||
|
hasher.write_u32(v.to_bits())
|
||||||
|
});
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds(&self, ctx: &PainterInstance) -> Rect {
|
||||||
|
let Rect { position, size } = self.child.bounds(ctx);
|
||||||
|
|
||||||
|
// XXX: to match the behavior above, transform the corners around the center
|
||||||
|
// FIXME: the behavior may not actually match?
|
||||||
|
|
||||||
|
let half = size / 2.0;
|
||||||
|
let center = position + half;
|
||||||
|
let points = [
|
||||||
|
self.transform.transform_point2(vec2(-half.x, -half.y)) + center,
|
||||||
|
self.transform.transform_point2(vec2( half.x, -half.y)) + center,
|
||||||
|
self.transform.transform_point2(vec2(-half.x, half.y)) + center,
|
||||||
|
self.transform.transform_point2(vec2( half.x, half.y)) + center,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut min_point = glam::Vec2::splat(f32::MAX);
|
||||||
|
let mut max_point = glam::Vec2::splat(f32::MIN);
|
||||||
|
for point in points {
|
||||||
|
min_point = min_point.min(point);
|
||||||
|
max_point = max_point.max(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
position: min_point,
|
||||||
|
size: max_point - min_point,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use nohash_hasher::BuildNoHashHasher;
|
||||||
|
|
||||||
pub(crate) type FontId = u16;
|
pub(crate) type FontId = u16;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub struct FontHandle(pub(crate) FontId);
|
pub struct FontHandle(pub(crate) FontId);
|
||||||
|
|
||||||
pub(crate) struct FontRepr {
|
pub(crate) struct FontRepr {
|
||||||
|
|
|
@ -76,7 +76,7 @@ type TextureId = u32;
|
||||||
///
|
///
|
||||||
/// Can be cheaply copied and passed around.\
|
/// Can be cheaply copied and passed around.\
|
||||||
/// The handle is only valid for the texture atlas it was created from.
|
/// The handle is only valid for the texture atlas it was created from.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||||
pub struct TextureHandle {
|
pub struct TextureHandle {
|
||||||
pub(crate) id: TextureId,
|
pub(crate) id: TextureId,
|
||||||
pub(crate) size: UVec2,
|
pub(crate) size: UVec2,
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
use std::hash::Hasher;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn hash_vec2(hasher: &mut impl Hasher, vec: glam::Vec2) {
|
||||||
|
hasher.write_u32(vec.x.to_bits());
|
||||||
|
hasher.write_u32(vec.y.to_bits());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn hash_vec3(hasher: &mut impl Hasher, vec: glam::Vec3) {
|
||||||
|
hasher.write_u32(vec.x.to_bits());
|
||||||
|
hasher.write_u32(vec.y.to_bits());
|
||||||
|
hasher.write_u32(vec.z.to_bits());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn hash_vec4(hasher: &mut impl Hasher, vec: glam::Vec4) {
|
||||||
|
hasher.write_u32(vec.x.to_bits());
|
||||||
|
hasher.write_u32(vec.y.to_bits());
|
||||||
|
hasher.write_u32(vec.z.to_bits());
|
||||||
|
hasher.write_u32(vec.w.to_bits());
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
|
use std::mem::ManuallyDrop;
|
||||||
|
|
||||||
/// Represents 4 corners of a rectangular shape.
|
/// Represents 4 corners of a rectangular shape.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct Corners<T> {
|
pub struct Corners<T> {
|
||||||
pub top_left: T,
|
pub top_left: T,
|
||||||
pub top_right: T,
|
pub top_right: T,
|
||||||
|
@ -7,6 +9,32 @@ pub struct Corners<T> {
|
||||||
pub bottom_right: T,
|
pub bottom_right: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Corners<T> {
|
||||||
|
#[inline]
|
||||||
|
pub fn to_array(self) -> [T; 4] {
|
||||||
|
[self.top_left, self.top_right, self.bottom_left, self.bottom_right]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn as_array(&self) -> [&T; 4] {
|
||||||
|
[
|
||||||
|
&self.top_left,
|
||||||
|
&self.top_right,
|
||||||
|
&self.bottom_left,
|
||||||
|
&self.bottom_right,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_array_mut(&mut self) -> [&mut T; 4] {
|
||||||
|
[
|
||||||
|
&mut self.top_left,
|
||||||
|
&mut self.top_right,
|
||||||
|
&mut self.bottom_left,
|
||||||
|
&mut self.bottom_right,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Clone> Corners<T> {
|
impl<T: Clone> Corners<T> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn all(value: T) -> Self {
|
pub fn all(value: T) -> Self {
|
||||||
|
@ -83,3 +111,61 @@ impl<T> From<(T, T, T, T)> for Corners<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> IntoIterator for Corners<T> {
|
||||||
|
type Item = T;
|
||||||
|
type IntoIter = std::array::IntoIter<Self::Item, 4>;
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.to_array().into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> IntoIterator for &'a Corners<T> {
|
||||||
|
type Item = &'a T;
|
||||||
|
type IntoIter = std::array::IntoIter<Self::Item, 4>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.as_array().into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> IntoIterator for &'a mut Corners<T> {
|
||||||
|
type Item = &'a mut T;
|
||||||
|
type IntoIter = std::array::IntoIter<Self::Item, 4>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.as_array_mut().into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// over-engineered :p
|
||||||
|
|
||||||
|
// struct CornersIter<T> {
|
||||||
|
// values: [ManuallyDrop<T>; 4],
|
||||||
|
// curr: u8,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<T> Iterator for CornersIter<T> {
|
||||||
|
// type Item = T;
|
||||||
|
|
||||||
|
// fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// if self.curr >= 4 {
|
||||||
|
// return None
|
||||||
|
// }
|
||||||
|
// let result = unsafe {
|
||||||
|
// ManuallyDrop::take(&mut self.values[self.curr as usize])
|
||||||
|
// };
|
||||||
|
// self.curr += 1;
|
||||||
|
// Some(result)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl<T> Drop for CornersIter<T> {
|
||||||
|
// fn drop(&mut self) {
|
||||||
|
// for i in self.curr..4 {
|
||||||
|
// unsafe {
|
||||||
|
// ManuallyDrop::drop(&mut self.values[i as usize]);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -11,6 +11,18 @@ pub struct Rect {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rect {
|
impl Rect {
|
||||||
|
/// A rect with both position and size set to zero.
|
||||||
|
pub const ZERO: Self = Self {
|
||||||
|
position: Vec2::ZERO,
|
||||||
|
size: Vec2::ZERO,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A rect with size of 1x1 and position of zero.
|
||||||
|
pub const UNIT: Self = Self {
|
||||||
|
position: Vec2::ZERO,
|
||||||
|
size: Vec2::ONE,
|
||||||
|
};
|
||||||
|
|
||||||
pub const fn new(position: Vec2, size: Vec2) -> Self {
|
pub const fn new(position: Vec2, size: Vec2) -> Self {
|
||||||
Self { position, size }
|
Self { position, size }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue