WIP
This commit is contained in:
parent
4cf2ba20c2
commit
cd1c137542
7 changed files with 235 additions and 15 deletions
|
@ -148,6 +148,7 @@ pub trait PlatformWindow {
|
||||||
fn draw(&self, scene: Scene);
|
fn draw(&self, scene: Scene);
|
||||||
|
|
||||||
fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||||
|
fn polychrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PlatformDispatcher: Send + Sync {
|
pub trait PlatformDispatcher: Send + Sync {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
point, size, AtlasTextureId, DevicePixels, MetalAtlas, MonochromeSprite, Quad, Scene, Size,
|
point, size, AtlasTextureId, DevicePixels, MetalAtlas, MonochromeSprite, PolychromeSprite,
|
||||||
|
Quad, Scene, Size,
|
||||||
};
|
};
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
base::{NO, YES},
|
base::{NO, YES},
|
||||||
|
@ -21,6 +22,7 @@ pub struct MetalRenderer {
|
||||||
unit_vertices: metal::Buffer,
|
unit_vertices: metal::Buffer,
|
||||||
instances: metal::Buffer,
|
instances: metal::Buffer,
|
||||||
monochrome_sprite_atlas: Arc<MetalAtlas>,
|
monochrome_sprite_atlas: Arc<MetalAtlas>,
|
||||||
|
polychrome_sprite_atlas: Arc<MetalAtlas>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetalRenderer {
|
impl MetalRenderer {
|
||||||
|
@ -107,6 +109,14 @@ impl MetalRenderer {
|
||||||
MTLPixelFormat::A8Unorm,
|
MTLPixelFormat::A8Unorm,
|
||||||
device.clone(),
|
device.clone(),
|
||||||
));
|
));
|
||||||
|
let polychrome_sprite_atlas = Arc::new(MetalAtlas::new(
|
||||||
|
Size {
|
||||||
|
width: DevicePixels(1024),
|
||||||
|
height: DevicePixels(768),
|
||||||
|
},
|
||||||
|
MTLPixelFormat::BGRA8Unorm,
|
||||||
|
device.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
layer,
|
layer,
|
||||||
|
@ -116,6 +126,7 @@ impl MetalRenderer {
|
||||||
unit_vertices,
|
unit_vertices,
|
||||||
instances,
|
instances,
|
||||||
monochrome_sprite_atlas,
|
monochrome_sprite_atlas,
|
||||||
|
polychrome_sprite_atlas,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +138,10 @@ impl MetalRenderer {
|
||||||
&self.monochrome_sprite_atlas
|
&self.monochrome_sprite_atlas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn polychrome_sprite_atlas(&self) -> &Arc<MetalAtlas> {
|
||||||
|
&self.polychrome_sprite_atlas
|
||||||
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self, scene: &mut Scene) {
|
pub fn draw(&mut self, scene: &mut Scene) {
|
||||||
let layer = self.layer.clone();
|
let layer = self.layer.clone();
|
||||||
let viewport_size = layer.drawable_size();
|
let viewport_size = layer.drawable_size();
|
||||||
|
@ -180,7 +195,7 @@ impl MetalRenderer {
|
||||||
command_encoder,
|
command_encoder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
crate::PrimitiveBatch::Sprites {
|
crate::PrimitiveBatch::MonochromeSprites {
|
||||||
texture_id,
|
texture_id,
|
||||||
sprites,
|
sprites,
|
||||||
} => {
|
} => {
|
||||||
|
@ -192,6 +207,18 @@ impl MetalRenderer {
|
||||||
command_encoder,
|
command_encoder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
crate::PrimitiveBatch::PolychromeSprites {
|
||||||
|
texture_id,
|
||||||
|
sprites,
|
||||||
|
} => {
|
||||||
|
self.draw_polychrome_sprites(
|
||||||
|
texture_id,
|
||||||
|
sprites,
|
||||||
|
&mut instance_offset,
|
||||||
|
viewport_size,
|
||||||
|
command_encoder,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,6 +364,81 @@ impl MetalRenderer {
|
||||||
);
|
);
|
||||||
*offset = next_offset;
|
*offset = next_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_polychrome_sprites(
|
||||||
|
&mut self,
|
||||||
|
texture_id: AtlasTextureId,
|
||||||
|
sprites: &[PolychromeSprite],
|
||||||
|
offset: &mut usize,
|
||||||
|
viewport_size: Size<DevicePixels>,
|
||||||
|
command_encoder: &metal::RenderCommandEncoderRef,
|
||||||
|
) {
|
||||||
|
todo!()
|
||||||
|
// if sprites.is_empty() {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// align_offset(offset);
|
||||||
|
|
||||||
|
// let texture = self.monochrome_sprite_atlas.texture(texture_id);
|
||||||
|
// let texture_size = size(
|
||||||
|
// DevicePixels(texture.width() as i32),
|
||||||
|
// DevicePixels(texture.height() as i32),
|
||||||
|
// );
|
||||||
|
// command_encoder.set_render_pipeline_state(&self.sprites_pipeline_state);
|
||||||
|
// command_encoder.set_vertex_buffer(
|
||||||
|
// MonochromeSpriteInputIndex::Vertices as u64,
|
||||||
|
// Some(&self.unit_vertices),
|
||||||
|
// 0,
|
||||||
|
// );
|
||||||
|
// command_encoder.set_vertex_buffer(
|
||||||
|
// MonochromeSpriteInputIndex::Sprites as u64,
|
||||||
|
// Some(&self.instances),
|
||||||
|
// *offset as u64,
|
||||||
|
// );
|
||||||
|
// command_encoder.set_vertex_bytes(
|
||||||
|
// MonochromeSpriteInputIndex::ViewportSize as u64,
|
||||||
|
// mem::size_of_val(&viewport_size) as u64,
|
||||||
|
// &viewport_size as *const Size<DevicePixels> as *const _,
|
||||||
|
// );
|
||||||
|
// command_encoder.set_vertex_bytes(
|
||||||
|
// MonochromeSpriteInputIndex::AtlasTextureSize as u64,
|
||||||
|
// mem::size_of_val(&texture_size) as u64,
|
||||||
|
// &texture_size as *const Size<DevicePixels> as *const _,
|
||||||
|
// );
|
||||||
|
// command_encoder.set_fragment_buffer(
|
||||||
|
// MonochromeSpriteInputIndex::Sprites as u64,
|
||||||
|
// Some(&self.instances),
|
||||||
|
// *offset as u64,
|
||||||
|
// );
|
||||||
|
// command_encoder.set_fragment_texture(
|
||||||
|
// MonochromeSpriteInputIndex::AtlasTexture as u64,
|
||||||
|
// Some(&texture),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let sprite_bytes_len = mem::size_of::<MonochromeSprite>() * sprites.len();
|
||||||
|
// let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
|
||||||
|
// unsafe {
|
||||||
|
// ptr::copy_nonoverlapping(
|
||||||
|
// sprites.as_ptr() as *const u8,
|
||||||
|
// buffer_contents,
|
||||||
|
// sprite_bytes_len,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let next_offset = *offset + sprite_bytes_len;
|
||||||
|
// assert!(
|
||||||
|
// next_offset <= INSTANCE_BUFFER_SIZE,
|
||||||
|
// "instance buffer exhausted"
|
||||||
|
// );
|
||||||
|
|
||||||
|
// command_encoder.draw_primitives_instanced(
|
||||||
|
// metal::MTLPrimitiveType::Triangle,
|
||||||
|
// 0,
|
||||||
|
// 6,
|
||||||
|
// sprites.len() as u64,
|
||||||
|
// );
|
||||||
|
// *offset = next_offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_pipeline_state(
|
fn build_pipeline_state(
|
||||||
|
|
|
@ -889,6 +889,10 @@ impl PlatformWindow for MacWindow {
|
||||||
fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||||
self.0.lock().renderer.monochrome_sprite_atlas().clone()
|
self.0.lock().renderer.monochrome_sprite_atlas().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn polychrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||||
|
self.0.lock().renderer.polychrome_sprite_atlas().clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_scale_factor(native_window: id) -> f32 {
|
fn get_scale_factor(native_window: id) -> f32 {
|
||||||
|
|
|
@ -38,8 +38,11 @@ impl Scene {
|
||||||
Primitive::Quad(quad) => {
|
Primitive::Quad(quad) => {
|
||||||
layer.quads.push(quad);
|
layer.quads.push(quad);
|
||||||
}
|
}
|
||||||
Primitive::Sprite(sprite) => {
|
Primitive::MonochromeSprite(sprite) => {
|
||||||
layer.sprites.push(sprite);
|
layer.monochrome_sprites.push(sprite);
|
||||||
|
}
|
||||||
|
Primitive::PolychromeSprite(sprite) => {
|
||||||
|
layer.polychrome_sprites.push(sprite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,19 +55,20 @@ impl Scene {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(crate) struct SceneLayer {
|
pub(crate) struct SceneLayer {
|
||||||
pub quads: Vec<Quad>,
|
pub quads: Vec<Quad>,
|
||||||
pub sprites: Vec<MonochromeSprite>,
|
pub monochrome_sprites: Vec<MonochromeSprite>,
|
||||||
|
pub polychrome_sprites: Vec<PolychromeSprite>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SceneLayer {
|
impl SceneLayer {
|
||||||
pub fn batches(&mut self) -> impl Iterator<Item = PrimitiveBatch> {
|
pub fn batches(&mut self) -> impl Iterator<Item = PrimitiveBatch> {
|
||||||
self.quads.sort_unstable();
|
self.quads.sort_unstable();
|
||||||
self.sprites.sort_unstable();
|
self.monochrome_sprites.sort_unstable();
|
||||||
|
|
||||||
BatchIterator::new(
|
BatchIterator::new(
|
||||||
&self.quads,
|
&self.quads,
|
||||||
self.quads.iter().peekable(),
|
self.quads.iter().peekable(),
|
||||||
&self.sprites,
|
&self.monochrome_sprites,
|
||||||
self.sprites.iter().peekable(),
|
self.monochrome_sprites.iter().peekable(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,7 +135,7 @@ where
|
||||||
})
|
})
|
||||||
.count();
|
.count();
|
||||||
self.sprites_start = sprites_end;
|
self.sprites_start = sprites_end;
|
||||||
Some(PrimitiveBatch::Sprites {
|
Some(PrimitiveBatch::MonochromeSprites {
|
||||||
texture_id,
|
texture_id,
|
||||||
sprites: &self.sprites[sprites_start..sprites_end],
|
sprites: &self.sprites[sprites_start..sprites_end],
|
||||||
})
|
})
|
||||||
|
@ -171,15 +175,20 @@ pub enum PrimitiveKind {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Primitive {
|
pub enum Primitive {
|
||||||
Quad(Quad),
|
Quad(Quad),
|
||||||
Sprite(MonochromeSprite),
|
MonochromeSprite(MonochromeSprite),
|
||||||
|
PolychromeSprite(PolychromeSprite),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum PrimitiveBatch<'a> {
|
pub(crate) enum PrimitiveBatch<'a> {
|
||||||
Quads(&'a [Quad]),
|
Quads(&'a [Quad]),
|
||||||
Sprites {
|
MonochromeSprites {
|
||||||
texture_id: AtlasTextureId,
|
texture_id: AtlasTextureId,
|
||||||
sprites: &'a [MonochromeSprite],
|
sprites: &'a [MonochromeSprite],
|
||||||
},
|
},
|
||||||
|
PolychromeSprites {
|
||||||
|
texture_id: AtlasTextureId,
|
||||||
|
sprites: &'a [PolychromeSprite],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
@ -256,7 +265,37 @@ impl PartialOrd for MonochromeSprite {
|
||||||
|
|
||||||
impl From<MonochromeSprite> for Primitive {
|
impl From<MonochromeSprite> for Primitive {
|
||||||
fn from(sprite: MonochromeSprite) -> Self {
|
fn from(sprite: MonochromeSprite) -> Self {
|
||||||
Primitive::Sprite(sprite)
|
Primitive::MonochromeSprite(sprite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct PolychromeSprite {
|
||||||
|
pub order: u32,
|
||||||
|
pub bounds: Bounds<ScaledPixels>,
|
||||||
|
pub content_mask: ScaledContentMask,
|
||||||
|
pub tile: AtlasTile,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for PolychromeSprite {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
match self.order.cmp(&other.order) {
|
||||||
|
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
|
||||||
|
order => order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for PolychromeSprite {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PolychromeSprite> for Primitive {
|
||||||
|
fn from(sprite: PolychromeSprite) -> Self {
|
||||||
|
Primitive::PolychromeSprite(sprite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -404,6 +404,25 @@ impl Hash for RenderGlyphParams {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct RenderEmojiParams {
|
||||||
|
pub(crate) font_id: FontId,
|
||||||
|
pub(crate) glyph_id: GlyphId,
|
||||||
|
pub(crate) font_size: Pixels,
|
||||||
|
pub(crate) scale_factor: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for RenderEmojiParams {}
|
||||||
|
|
||||||
|
impl Hash for RenderEmojiParams {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.font_id.0.hash(state);
|
||||||
|
self.glyph_id.0.hash(state);
|
||||||
|
self.font_size.0.to_bits().hash(state);
|
||||||
|
self.scale_factor.to_bits().hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
pub family: SharedString,
|
pub family: SharedString,
|
||||||
|
|
|
@ -159,7 +159,13 @@ impl Line {
|
||||||
}
|
}
|
||||||
|
|
||||||
if glyph.is_emoji {
|
if glyph.is_emoji {
|
||||||
todo!()
|
cx.paint_emoji(
|
||||||
|
glyph_origin,
|
||||||
|
layout.order,
|
||||||
|
run.font_id,
|
||||||
|
glyph.id,
|
||||||
|
self.layout.font_size,
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
cx.paint_glyph(
|
cx.paint_glyph(
|
||||||
glyph_origin,
|
glyph_origin,
|
||||||
|
|
|
@ -2,8 +2,8 @@ use crate::{
|
||||||
px, AnyView, AppContext, AvailableSpace, BorrowAppContext, Bounds, Context, Corners,
|
px, AnyView, AppContext, AvailableSpace, BorrowAppContext, Bounds, Context, Corners,
|
||||||
DevicePixels, Effect, Element, EntityId, FontId, GlyphId, Handle, Hsla, IsZero, LayerId,
|
DevicePixels, Effect, Element, EntityId, FontId, GlyphId, Handle, Hsla, IsZero, LayerId,
|
||||||
LayoutId, MainThread, MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow,
|
LayoutId, MainThread, MainThreadOnly, MonochromeSprite, Pixels, PlatformAtlas, PlatformWindow,
|
||||||
Point, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size,
|
Point, PolychromeSprite, Reference, RenderGlyphParams, RenderSvgParams, ScaledPixels, Scene,
|
||||||
Style, TaffyLayoutEngine, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
|
SharedString, Size, Style, TaffyLayoutEngine, WeakHandle, WindowOptions, SUBPIXEL_VARIANTS,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
|
@ -17,6 +17,7 @@ pub struct Window {
|
||||||
handle: AnyWindowHandle,
|
handle: AnyWindowHandle,
|
||||||
platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
|
platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
|
||||||
monochrome_sprite_atlas: Arc<dyn PlatformAtlas>,
|
monochrome_sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||||
|
polychrome_sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||||
rem_size: Pixels,
|
rem_size: Pixels,
|
||||||
content_size: Size<Pixels>,
|
content_size: Size<Pixels>,
|
||||||
layout_engine: TaffyLayoutEngine,
|
layout_engine: TaffyLayoutEngine,
|
||||||
|
@ -36,6 +37,7 @@ impl Window {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let platform_window = cx.platform().open_window(handle, options);
|
let platform_window = cx.platform().open_window(handle, options);
|
||||||
let monochrome_sprite_atlas = platform_window.monochrome_sprite_atlas();
|
let monochrome_sprite_atlas = platform_window.monochrome_sprite_atlas();
|
||||||
|
let polychrome_sprite_atlas = platform_window.polychrome_sprite_atlas();
|
||||||
let mouse_position = platform_window.mouse_position();
|
let mouse_position = platform_window.mouse_position();
|
||||||
let content_size = platform_window.content_size();
|
let content_size = platform_window.content_size();
|
||||||
let scale_factor = platform_window.scale_factor();
|
let scale_factor = platform_window.scale_factor();
|
||||||
|
@ -59,6 +61,7 @@ impl Window {
|
||||||
handle,
|
handle,
|
||||||
platform_window,
|
platform_window,
|
||||||
monochrome_sprite_atlas,
|
monochrome_sprite_atlas,
|
||||||
|
polychrome_sprite_atlas,
|
||||||
rem_size: px(16.),
|
rem_size: px(16.),
|
||||||
content_size,
|
content_size,
|
||||||
layout_engine: TaffyLayoutEngine::new(),
|
layout_engine: TaffyLayoutEngine::new(),
|
||||||
|
@ -300,6 +303,52 @@ impl<'a, 'w> WindowContext<'a, 'w> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn paint_emoji(
|
||||||
|
&mut self,
|
||||||
|
origin: Point<Pixels>,
|
||||||
|
order: u32,
|
||||||
|
font_id: FontId,
|
||||||
|
glyph_id: GlyphId,
|
||||||
|
font_size: Pixels,
|
||||||
|
) -> Result<()> {
|
||||||
|
let scale_factor = self.scale_factor();
|
||||||
|
let glyph_origin = origin.scale(scale_factor);
|
||||||
|
let params = RenderGlyphParams {
|
||||||
|
font_id,
|
||||||
|
glyph_id,
|
||||||
|
font_size,
|
||||||
|
subpixel_variant: Default::default(),
|
||||||
|
scale_factor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let raster_bounds = self.text_system().raster_bounds(¶ms)?;
|
||||||
|
if !raster_bounds.is_zero() {
|
||||||
|
let layer_id = self.current_layer_id();
|
||||||
|
let tile = self
|
||||||
|
.window
|
||||||
|
.polychrome_sprite_atlas
|
||||||
|
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||||
|
self.text_system().rasterize_glyph(¶ms)
|
||||||
|
})?;
|
||||||
|
let bounds = Bounds {
|
||||||
|
origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
|
||||||
|
size: tile.bounds.size.map(Into::into),
|
||||||
|
};
|
||||||
|
let content_mask = self.content_mask().scale(scale_factor);
|
||||||
|
|
||||||
|
self.window.scene.insert(
|
||||||
|
layer_id,
|
||||||
|
PolychromeSprite {
|
||||||
|
order,
|
||||||
|
bounds,
|
||||||
|
content_mask,
|
||||||
|
tile,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn draw(&mut self) -> Result<()> {
|
pub(crate) fn draw(&mut self) -> Result<()> {
|
||||||
let unit_entity = self.unit_entity.clone();
|
let unit_entity = self.unit_entity.clone();
|
||||||
self.update_entity(&unit_entity, |_, cx| {
|
self.update_entity(&unit_entity, |_, cx| {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue