Detailed changes
@@ -48,10 +48,11 @@ fn generate_shader_bindings() -> PathBuf {
"ScaledContentMask".into(),
"Uniforms".into(),
"AtlasTile".into(),
- "Quad".into(),
"QuadInputIndex".into(),
+ "Quad".into(),
+ "SpriteInputIndex".into(),
"MonochromeSprite".into(),
- "MonochromeSpriteInputIndex".into(),
+ "PolychromeSprite".into(),
]);
config.no_includes = true;
config.enumeration.prefix_with_name = true;
@@ -147,8 +147,7 @@ pub trait PlatformWindow {
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn draw(&self, scene: Scene);
- fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
- fn polychrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
+ fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
}
pub trait PlatformDispatcher: Send + Sync {
@@ -182,6 +181,15 @@ pub enum AtlasKey {
Svg(RenderSvgParams),
}
+impl AtlasKey {
+ pub fn is_monochrome(&self) -> bool {
+ match self {
+ AtlasKey::Glyph(params) => !params.is_emoji,
+ AtlasKey::Svg(_) => true,
+ }
+ }
+}
+
impl From<RenderGlyphParams> for AtlasKey {
fn from(params: RenderGlyphParams) -> Self {
Self::Glyph(params)
@@ -250,12 +258,6 @@ pub trait PlatformInputHandler {
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ScreenId(pub(crate) Uuid);
-#[derive(Copy, Clone, Debug)]
-pub enum RasterizationOptions {
- Alpha,
- Bgra,
-}
-
#[derive(Debug)]
pub struct WindowOptions {
pub bounds: WindowBounds,
@@ -5,26 +5,15 @@ use anyhow::{anyhow, Result};
use collections::HashMap;
use derive_more::{Deref, DerefMut};
use etagere::BucketedAtlasAllocator;
-use foreign_types::ForeignType;
-use metal::{Device, TextureDescriptor};
-use objc::{msg_send, sel, sel_impl};
+use metal::Device;
use parking_lot::Mutex;
pub struct MetalAtlas(Mutex<MetalAtlasState>);
impl MetalAtlas {
- pub fn new(
- size: Size<DevicePixels>,
- pixel_format: metal::MTLPixelFormat,
- device: Device,
- ) -> Self {
- let texture_descriptor = metal::TextureDescriptor::new();
- texture_descriptor.set_pixel_format(pixel_format);
- texture_descriptor.set_width(size.width.into());
- texture_descriptor.set_height(size.height.into());
+ pub fn new(device: Device) -> Self {
MetalAtlas(Mutex::new(MetalAtlasState {
device: AssertSend(device),
- texture_descriptor: AssertSend(texture_descriptor),
textures: Default::default(),
tiles_by_key: Default::default(),
}))
@@ -37,7 +26,6 @@ impl MetalAtlas {
struct MetalAtlasState {
device: AssertSend<Device>,
- texture_descriptor: AssertSend<TextureDescriptor>,
textures: Vec<MetalAtlasTexture>,
tiles_by_key: HashMap<AtlasKey, AtlasTile>,
}
@@ -57,9 +45,15 @@ impl PlatformAtlas for MetalAtlas {
.textures
.iter_mut()
.rev()
- .find_map(|texture| texture.upload(size, &bytes))
+ .find_map(|texture| {
+ if texture.monochrome == key.is_monochrome() {
+ texture.upload(size, &bytes)
+ } else {
+ None
+ }
+ })
.or_else(|| {
- let texture = lock.push_texture(size);
+ let texture = lock.push_texture(size, key.is_monochrome());
texture.upload(size, &bytes)
})
.ok_or_else(|| anyhow!("could not allocate in new texture"))?;
@@ -74,35 +68,32 @@ impl PlatformAtlas for MetalAtlas {
}
impl MetalAtlasState {
- fn push_texture(&mut self, min_size: Size<DevicePixels>) -> &mut MetalAtlasTexture {
- let default_atlas_size = Size {
- width: self.texture_descriptor.width().into(),
- height: self.texture_descriptor.height().into(),
+ fn push_texture(
+ &mut self,
+ min_size: Size<DevicePixels>,
+ monochrome: bool,
+ ) -> &mut MetalAtlasTexture {
+ const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
+ width: DevicePixels(1024),
+ height: DevicePixels(1024),
};
- let size;
- let metal_texture;
-
- if min_size.width > default_atlas_size.width || min_size.height > default_atlas_size.height
- {
- let descriptor = unsafe {
- let descriptor_ptr: *mut metal::MTLTextureDescriptor =
- msg_send![*self.texture_descriptor, copy];
- metal::TextureDescriptor::from_ptr(descriptor_ptr)
- };
- descriptor.set_width(min_size.width.into());
- descriptor.set_height(min_size.height.into());
- descriptor.set_pixel_format(metal::MTLPixelFormat::Depth32Float);
- size = min_size;
- metal_texture = self.device.new_texture(&descriptor);
+
+ let size = min_size.max(&DEFAULT_ATLAS_SIZE);
+ let texture_descriptor = metal::TextureDescriptor::new();
+ texture_descriptor.set_width(size.width.into());
+ texture_descriptor.set_height(size.height.into());
+ if monochrome {
+ texture_descriptor.set_pixel_format(metal::MTLPixelFormat::A8Unorm);
} else {
- size = default_atlas_size;
- metal_texture = self.device.new_texture(&self.texture_descriptor);
+ texture_descriptor.set_pixel_format(metal::MTLPixelFormat::BGRA8Unorm);
}
+ let metal_texture = self.device.new_texture(&texture_descriptor);
let atlas_texture = MetalAtlasTexture {
id: AtlasTextureId(self.textures.len() as u32),
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
metal_texture: AssertSend(metal_texture),
+ monochrome,
};
self.textures.push(atlas_texture);
self.textures.last_mut().unwrap()
@@ -113,6 +104,7 @@ struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
+ monochrome: bool,
}
impl MetalAtlasTexture {
@@ -18,11 +18,11 @@ pub struct MetalRenderer {
layer: metal::MetalLayer,
command_queue: CommandQueue,
quads_pipeline_state: metal::RenderPipelineState,
- sprites_pipeline_state: metal::RenderPipelineState,
+ monochrome_sprites_pipeline_state: metal::RenderPipelineState,
+ polychrome_sprites_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
instances: metal::Buffer,
- monochrome_sprite_atlas: Arc<MetalAtlas>,
- polychrome_sprite_atlas: Arc<MetalAtlas>,
+ sprite_atlas: Arc<MetalAtlas>,
}
impl MetalRenderer {
@@ -90,43 +90,35 @@ impl MetalRenderer {
"quad_fragment",
PIXEL_FORMAT,
);
-
- let sprites_pipeline_state = build_pipeline_state(
+ let monochrome_sprites_pipeline_state = build_pipeline_state(
&device,
&library,
- "sprites",
+ "monochrome_sprites",
"monochrome_sprite_vertex",
"monochrome_sprite_fragment",
PIXEL_FORMAT,
);
+ let polychrome_sprites_pipeline_state = build_pipeline_state(
+ &device,
+ &library,
+ "polychrome_sprites",
+ "polychrome_sprite_vertex",
+ "polychrome_sprite_fragment",
+ PIXEL_FORMAT,
+ );
let command_queue = device.new_command_queue();
- let monochrome_sprite_atlas = Arc::new(MetalAtlas::new(
- Size {
- width: DevicePixels(1024),
- height: DevicePixels(768),
- },
- MTLPixelFormat::A8Unorm,
- device.clone(),
- ));
- let polychrome_sprite_atlas = Arc::new(MetalAtlas::new(
- Size {
- width: DevicePixels(1024),
- height: DevicePixels(768),
- },
- MTLPixelFormat::BGRA8Unorm,
- device.clone(),
- ));
+ let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
Self {
layer,
command_queue,
quads_pipeline_state,
- sprites_pipeline_state,
+ monochrome_sprites_pipeline_state,
+ polychrome_sprites_pipeline_state,
unit_vertices,
instances,
- monochrome_sprite_atlas,
- polychrome_sprite_atlas,
+ sprite_atlas,
}
}
@@ -134,12 +126,8 @@ impl MetalRenderer {
&*self.layer
}
- pub fn monochrome_sprite_atlas(&self) -> &Arc<MetalAtlas> {
- &self.monochrome_sprite_atlas
- }
-
- pub fn polychrome_sprite_atlas(&self) -> &Arc<MetalAtlas> {
- &self.polychrome_sprite_atlas
+ pub fn sprite_atlas(&self) -> &Arc<MetalAtlas> {
+ &self.sprite_atlas
}
pub fn draw(&mut self, scene: &mut Scene) {
@@ -304,41 +292,38 @@ impl MetalRenderer {
}
align_offset(offset);
- let texture = self.monochrome_sprite_atlas.texture(texture_id);
+ let texture = self.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_render_pipeline_state(&self.monochrome_sprites_pipeline_state);
command_encoder.set_vertex_buffer(
- MonochromeSpriteInputIndex::Vertices as u64,
+ SpriteInputIndex::Vertices as u64,
Some(&self.unit_vertices),
0,
);
command_encoder.set_vertex_buffer(
- MonochromeSpriteInputIndex::Sprites as u64,
+ SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
);
command_encoder.set_vertex_bytes(
- MonochromeSpriteInputIndex::ViewportSize as u64,
+ SpriteInputIndex::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,
+ SpriteInputIndex::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,
+ SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
);
- command_encoder.set_fragment_texture(
- MonochromeSpriteInputIndex::AtlasTexture as u64,
- Some(&texture),
- );
+ command_encoder.set_fragment_texture(SpriteInputIndex::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) };
@@ -373,71 +358,67 @@ impl MetalRenderer {
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;
+ if sprites.is_empty() {
+ return;
+ }
+ align_offset(offset);
+
+ let texture = self.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.polychrome_sprites_pipeline_state);
+ command_encoder.set_vertex_buffer(
+ SpriteInputIndex::Vertices as u64,
+ Some(&self.unit_vertices),
+ 0,
+ );
+ command_encoder.set_vertex_buffer(
+ SpriteInputIndex::Sprites as u64,
+ Some(&self.instances),
+ *offset as u64,
+ );
+ command_encoder.set_vertex_bytes(
+ SpriteInputIndex::ViewportSize as u64,
+ mem::size_of_val(&viewport_size) as u64,
+ &viewport_size as *const Size<DevicePixels> as *const _,
+ );
+ command_encoder.set_vertex_bytes(
+ SpriteInputIndex::AtlasTextureSize as u64,
+ mem::size_of_val(&texture_size) as u64,
+ &texture_size as *const Size<DevicePixels> as *const _,
+ );
+ command_encoder.set_fragment_buffer(
+ SpriteInputIndex::Sprites as u64,
+ Some(&self.instances),
+ *offset as u64,
+ );
+ command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
+
+ let sprite_bytes_len = mem::size_of::<PolychromeSprite>() * 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;
}
}
@@ -489,7 +470,7 @@ enum QuadInputIndex {
}
#[repr(C)]
-enum MonochromeSpriteInputIndex {
+enum SpriteInputIndex {
Vertices = 0,
Sprites = 1,
ViewportSize = 2,
@@ -7,7 +7,10 @@ float4 hsla_to_rgba(Hsla hsla);
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
Bounds_ScaledPixels clip_bounds,
constant Size_DevicePixels *viewport_size);
-float quad_sdf(float2 point, Bounds_ScaledPixels bounds, Corners_ScaledPixels corner_radii);
+float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
+ constant Size_DevicePixels *atlas_size);
+float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
+ Corners_ScaledPixels corner_radii);
struct QuadVertexOutput {
float4 position [[position]];
@@ -119,27 +122,18 @@ struct MonochromeSpriteVertexOutput {
vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
- constant float2 *unit_vertices
- [[buffer(MonochromeSpriteInputIndex_Vertices)]],
- constant MonochromeSprite *sprites
- [[buffer(MonochromeSpriteInputIndex_Sprites)]],
+ constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
+ constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
constant Size_DevicePixels *viewport_size
- [[buffer(MonochromeSpriteInputIndex_ViewportSize)]],
+ [[buffer(SpriteInputIndex_ViewportSize)]],
constant Size_DevicePixels *atlas_size
- [[buffer(MonochromeSpriteInputIndex_AtlasTextureSize)]]) {
+ [[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
MonochromeSprite sprite = sprites[sprite_id];
float4 device_position = to_device_position(
unit_vertex, sprite.bounds, sprite.content_mask.bounds, viewport_size);
-
- float2 tile_origin =
- float2(sprite.tile.bounds.origin.x, sprite.tile.bounds.origin.y);
- float2 tile_size =
- float2(sprite.tile.bounds.size.width, sprite.tile.bounds.size.height);
- float2 tile_position =
- (tile_origin + unit_vertex * tile_size) /
- float2((float)atlas_size->width, (float)atlas_size->height);
+ float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
float4 color = hsla_to_rgba(sprite.color);
return MonochromeSpriteVertexOutput{device_position, tile_position, color,
sprite_id};
@@ -147,22 +141,60 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
fragment float4 monochrome_sprite_fragment(
MonochromeSpriteVertexOutput input [[stage_in]],
- constant MonochromeSprite *sprites
- [[buffer(MonochromeSpriteInputIndex_Sprites)]],
- texture2d<float> atlas_texture
- [[texture(MonochromeSpriteInputIndex_AtlasTexture)]]) {
+ constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+ texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
MonochromeSprite sprite = sprites[input.sprite_id];
constexpr sampler atlas_texture_sampler(mag_filter::linear,
min_filter::linear);
float4 sample =
atlas_texture.sample(atlas_texture_sampler, input.tile_position);
- float clip_distance =
- quad_sdf(input.position.xy, sprite.content_mask.bounds, sprite.content_mask.corner_radii);
+ float clip_distance = quad_sdf(input.position.xy, sprite.content_mask.bounds,
+ sprite.content_mask.corner_radii);
float4 color = input.color;
color.a *= sample.a * saturate(0.5 - clip_distance);
return color;
}
+struct PolychromeSpriteVertexOutput {
+ float4 position [[position]];
+ float2 tile_position;
+ uint sprite_id [[flat]];
+};
+
+vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex(
+ uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]],
+ constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]],
+ constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+ constant Size_DevicePixels *viewport_size
+ [[buffer(SpriteInputIndex_ViewportSize)]],
+ constant Size_DevicePixels *atlas_size
+ [[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
+
+ float2 unit_vertex = unit_vertices[unit_vertex_id];
+ PolychromeSprite sprite = sprites[sprite_id];
+ float4 device_position = to_device_position(
+ unit_vertex, sprite.bounds, sprite.content_mask.bounds, viewport_size);
+ float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
+ return PolychromeSpriteVertexOutput{device_position, tile_position,
+ sprite_id};
+}
+
+fragment float4 polychrome_sprite_fragment(
+ PolychromeSpriteVertexOutput input [[stage_in]],
+ constant PolychromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
+ texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
+ PolychromeSprite sprite = sprites[input.sprite_id];
+ constexpr sampler atlas_texture_sampler(mag_filter::linear,
+ min_filter::linear);
+ float4 sample =
+ atlas_texture.sample(atlas_texture_sampler, input.tile_position);
+ float clip_distance = quad_sdf(input.position.xy, sprite.content_mask.bounds,
+ sprite.content_mask.corner_radii);
+ float4 color = sample;
+ color.a *= saturate(0.5 - clip_distance);
+ return color;
+}
+
float4 hsla_to_rgba(Hsla hsla) {
float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
float s = hsla.s;
@@ -229,6 +261,14 @@ float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
return float4(device_position, 0., 1.);
}
+float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
+ constant Size_DevicePixels *atlas_size) {
+ float2 tile_origin = float2(tile.bounds.origin.x, tile.bounds.origin.y);
+ float2 tile_size = float2(tile.bounds.size.width, tile.bounds.size.height);
+ return (tile_origin + unit_vertex * tile_size) /
+ float2((float)atlas_size->width, (float)atlas_size->height);
+}
+
float quad_sdf(float2 point, Bounds_ScaledPixels bounds,
Corners_ScaledPixels corner_radii) {
float2 half_size = float2(bounds.size.width, bounds.size.height) / 2.;
@@ -12,7 +12,11 @@ use core_foundation::{
base::{CFRange, TCFType},
string::CFString,
};
-use core_graphics::{base::CGGlyph, color_space::CGColorSpace, context::CGContext};
+use core_graphics::{
+ base::{kCGImageAlphaPremultipliedLast, CGGlyph},
+ color_space::CGColorSpace,
+ context::CGContext,
+};
use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
use font_kit::{
font::Font as FontKitFont,
@@ -262,16 +266,31 @@ impl MacTextSystemState {
bitmap_size.height += DevicePixels(1);
}
- let mut bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];
- let cx = CGContext::create_bitmap_context(
- Some(bytes.as_mut_ptr() as *mut _),
- bitmap_size.width.0 as usize,
- bitmap_size.height.0 as usize,
- 8,
- bitmap_size.width.0 as usize,
- &CGColorSpace::create_device_gray(),
- kCGImageAlphaOnly,
- );
+ let mut bytes;
+ let cx;
+ if params.is_emoji {
+ bytes = vec![0; bitmap_size.width.0 as usize * 4 * bitmap_size.height.0 as usize];
+ cx = CGContext::create_bitmap_context(
+ Some(bytes.as_mut_ptr() as *mut _),
+ bitmap_size.width.0 as usize,
+ bitmap_size.height.0 as usize,
+ 8,
+ bitmap_size.width.0 as usize * 4,
+ &CGColorSpace::create_device_rgb(),
+ kCGImageAlphaPremultipliedLast,
+ );
+ } else {
+ bytes = vec![0; bitmap_size.width.0 as usize * bitmap_size.height.0 as usize];
+ cx = CGContext::create_bitmap_context(
+ Some(bytes.as_mut_ptr() as *mut _),
+ bitmap_size.width.0 as usize,
+ bitmap_size.height.0 as usize,
+ 8,
+ bitmap_size.width.0 as usize,
+ &CGColorSpace::create_device_gray(),
+ kCGImageAlphaOnly,
+ );
+ }
// Move the origin to bottom left and account for scaling, this
// makes drawing text consistent with the font-kit's raster_bounds.
@@ -303,6 +322,17 @@ impl MacTextSystemState {
cx,
);
+ if params.is_emoji {
+ // Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
+ for pixel in bytes.chunks_exact_mut(4) {
+ pixel.swap(0, 2);
+ let a = pixel[3] as f32 / 255.;
+ pixel[0] = (pixel[0] as f32 / a) as u8;
+ pixel[1] = (pixel[1] as f32 / a) as u8;
+ pixel[2] = (pixel[2] as f32 / a) as u8;
+ }
+ }
+
Ok((bitmap_size.into(), bytes))
}
}
@@ -886,12 +886,8 @@ impl PlatformWindow for MacWindow {
}
}
- fn monochrome_sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
- 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 sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
+ self.0.lock().renderer.sprite_atlas().clone()
}
}
@@ -1,4 +1,4 @@
-use std::{iter::Peekable, mem};
+use std::{iter::Peekable, mem, slice};
use super::{Bounds, Hsla, Point};
use crate::{AtlasTextureId, AtlasTile, Corners, Edges, ScaledContentMask, ScaledPixels};
@@ -63,42 +63,46 @@ impl SceneLayer {
pub fn batches(&mut self) -> impl Iterator<Item = PrimitiveBatch> {
self.quads.sort_unstable();
self.monochrome_sprites.sort_unstable();
-
- BatchIterator::new(
- &self.quads,
- self.quads.iter().peekable(),
- &self.monochrome_sprites,
- self.monochrome_sprites.iter().peekable(),
- )
+ self.polychrome_sprites.sort_unstable();
+ BatchIterator {
+ quads: &self.quads,
+ quads_start: 0,
+ quads_iter: self.quads.iter().peekable(),
+ monochrome_sprites: &self.monochrome_sprites,
+ monochrome_sprites_start: 0,
+ monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
+ polychrome_sprites: &self.polychrome_sprites,
+ polychrome_sprites_start: 0,
+ polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
+ }
}
}
-struct BatchIterator<'a, Q, S>
-where
- Q: Iterator<Item = &'a Quad>,
- S: Iterator<Item = &'a MonochromeSprite>,
-{
+struct BatchIterator<'a> {
quads: &'a [Quad],
- sprites: &'a [MonochromeSprite],
quads_start: usize,
- sprites_start: usize,
- quads_iter: Peekable<Q>,
- sprites_iter: Peekable<S>,
+ quads_iter: Peekable<slice::Iter<'a, Quad>>,
+ monochrome_sprites: &'a [MonochromeSprite],
+ monochrome_sprites_start: usize,
+ monochrome_sprites_iter: Peekable<slice::Iter<'a, MonochromeSprite>>,
+ polychrome_sprites: &'a [PolychromeSprite],
+ polychrome_sprites_start: usize,
+ polychrome_sprites_iter: Peekable<slice::Iter<'a, PolychromeSprite>>,
}
-impl<'a, Q: 'a, S: 'a> Iterator for BatchIterator<'a, Q, S>
-where
- Q: Iterator<Item = &'a Quad>,
- S: Iterator<Item = &'a MonochromeSprite>,
-{
+impl<'a> Iterator for BatchIterator<'a> {
type Item = PrimitiveBatch<'a>;
fn next(&mut self) -> Option<Self::Item> {
let mut kinds_and_orders = [
(PrimitiveKind::Quad, self.quads_iter.peek().map(|q| q.order)),
(
- PrimitiveKind::Sprite,
- self.sprites_iter.peek().map(|s| s.order),
+ PrimitiveKind::MonochromeSprite,
+ self.monochrome_sprites_iter.peek().map(|s| s.order),
+ ),
+ (
+ PrimitiveKind::PolychromeSprite,
+ self.polychrome_sprites_iter.peek().map(|s| s.order),
),
];
kinds_and_orders.sort_by_key(|(_, order)| order.unwrap_or(u32::MAX));
@@ -123,45 +127,40 @@ where
self.quads_start = quads_end;
Some(PrimitiveBatch::Quads(&self.quads[quads_start..quads_end]))
}
- PrimitiveKind::Sprite => {
- let texture_id = self.sprites_iter.peek().unwrap().tile.texture_id;
- let sprites_start = self.sprites_start;
+ PrimitiveKind::MonochromeSprite => {
+ let texture_id = self.monochrome_sprites_iter.peek().unwrap().tile.texture_id;
+ let sprites_start = self.monochrome_sprites_start;
let sprites_end = sprites_start
+ self
- .sprites_iter
+ .monochrome_sprites_iter
.by_ref()
.take_while(|sprite| {
sprite.order <= max_order && sprite.tile.texture_id == texture_id
})
.count();
- self.sprites_start = sprites_end;
+ self.monochrome_sprites_start = sprites_end;
Some(PrimitiveBatch::MonochromeSprites {
texture_id,
- sprites: &self.sprites[sprites_start..sprites_end],
+ sprites: &self.monochrome_sprites[sprites_start..sprites_end],
+ })
+ }
+ PrimitiveKind::PolychromeSprite => {
+ let texture_id = self.polychrome_sprites_iter.peek().unwrap().tile.texture_id;
+ let sprites_start = self.polychrome_sprites_start;
+ let sprites_end = sprites_start
+ + self
+ .polychrome_sprites_iter
+ .by_ref()
+ .take_while(|sprite| {
+ sprite.order <= max_order && sprite.tile.texture_id == texture_id
+ })
+ .count();
+ self.polychrome_sprites_start = sprites_end;
+ Some(PrimitiveBatch::PolychromeSprites {
+ texture_id,
+ sprites: &self.polychrome_sprites[sprites_start..sprites_end],
})
}
- }
- }
-}
-
-impl<'a, Q: 'a, S: 'a> BatchIterator<'a, Q, S>
-where
- Q: Iterator<Item = &'a Quad>,
- S: Iterator<Item = &'a MonochromeSprite>,
-{
- fn new(
- quads: &'a [Quad],
- quads_iter: Peekable<Q>,
- sprites: &'a [MonochromeSprite],
- sprites_iter: Peekable<S>,
- ) -> Self {
- Self {
- quads,
- quads_start: 0,
- quads_iter,
- sprites,
- sprites_start: 0,
- sprites_iter,
}
}
}
@@ -169,7 +168,8 @@ where
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PrimitiveKind {
Quad,
- Sprite,
+ MonochromeSprite,
+ PolychromeSprite,
}
#[derive(Clone, Debug)]
@@ -36,8 +36,7 @@ pub struct TextSystem {
text_layout_cache: Arc<TextLayoutCache>,
platform_text_system: Arc<dyn PlatformTextSystem>,
font_ids_by_font: RwLock<HashMap<Font, FontId>>,
- fonts_by_font_id: RwLock<HashMap<FontId, Font>>,
- font_metrics: RwLock<HashMap<Font, FontMetrics>>,
+ font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
font_runs_pool: Mutex<Vec<Vec<(usize, FontId)>>>,
}
@@ -49,7 +48,6 @@ impl TextSystem {
platform_text_system,
font_metrics: RwLock::new(HashMap::default()),
font_ids_by_font: RwLock::new(HashMap::default()),
- fonts_by_font_id: RwLock::new(HashMap::default()),
wrapper_pool: Mutex::new(HashMap::default()),
font_runs_pool: Default::default(),
}
@@ -62,30 +60,20 @@ impl TextSystem {
} else {
let font_id = self.platform_text_system.font_id(font)?;
self.font_ids_by_font.write().insert(font.clone(), font_id);
- self.fonts_by_font_id.write().insert(font_id, font.clone());
Ok(font_id)
}
}
- pub fn with_font<T>(&self, font_id: FontId, f: impl FnOnce(&Self, &Font) -> T) -> Result<T> {
- self.fonts_by_font_id
- .read()
- .get(&font_id)
- .ok_or_else(|| anyhow!("font not found"))
- .map(|font| f(self, font))
- }
-
- pub fn bounding_box(&self, font: &Font, font_size: Pixels) -> Result<Bounds<Pixels>> {
- self.read_metrics(&font, |metrics| metrics.bounding_box(font_size))
+ pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result<Bounds<Pixels>> {
+ self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
}
pub fn typographic_bounds(
&self,
- font: &Font,
+ font_id: FontId,
font_size: Pixels,
character: char,
) -> Result<Bounds<Pixels>> {
- let font_id = self.font_id(font)?;
let glyph_id = self
.platform_text_system
.glyph_for_char(font_id, character)
@@ -93,65 +81,63 @@ impl TextSystem {
let bounds = self
.platform_text_system
.typographic_bounds(font_id, glyph_id)?;
- self.read_metrics(font, |metrics| {
+ self.read_metrics(font_id, |metrics| {
(bounds / metrics.units_per_em as f32 * font_size.0).map(px)
})
}
- pub fn advance(&self, font: &Font, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
- let font_id = self.font_id(font)?;
+ pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
let glyph_id = self
.platform_text_system
.glyph_for_char(font_id, ch)
.ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?;
- let result =
- self.platform_text_system.advance(font_id, glyph_id)? / self.units_per_em(font)? as f32;
+ let result = self.platform_text_system.advance(font_id, glyph_id)?
+ / self.units_per_em(font_id)? as f32;
Ok(result * font_size)
}
- pub fn units_per_em(&self, font: &Font) -> Result<u32> {
- self.read_metrics(font, |metrics| metrics.units_per_em as u32)
+ pub fn units_per_em(&self, font_id: FontId) -> Result<u32> {
+ self.read_metrics(font_id, |metrics| metrics.units_per_em as u32)
}
- pub fn cap_height(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
- self.read_metrics(font, |metrics| metrics.cap_height(font_size))
+ pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
}
- pub fn x_height(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
- self.read_metrics(font, |metrics| metrics.x_height(font_size))
+ pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
}
- pub fn ascent(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
- self.read_metrics(font, |metrics| metrics.ascent(font_size))
+ pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
}
- pub fn descent(&self, font: &Font, font_size: Pixels) -> Result<Pixels> {
- self.read_metrics(font, |metrics| metrics.descent(font_size))
+ pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ self.read_metrics(font_id, |metrics| metrics.descent(font_size))
}
pub fn baseline_offset(
&self,
- font: &Font,
+ font_id: FontId,
font_size: Pixels,
line_height: Pixels,
) -> Result<Pixels> {
- let ascent = self.ascent(font, font_size)?;
- let descent = self.descent(font, font_size)?;
+ let ascent = self.ascent(font_id, font_size)?;
+ let descent = self.descent(font_id, font_size)?;
let padding_top = (line_height - ascent - descent) / 2.;
Ok(padding_top + ascent)
}
- fn read_metrics<T>(&self, font: &Font, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
+ fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
let lock = self.font_metrics.upgradable_read();
- if let Some(metrics) = lock.get(font) {
+ if let Some(metrics) = lock.get(&font_id) {
Ok(read(metrics))
} else {
- let font_id = self.platform_text_system.font_id(&font)?;
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
let metrics = lock
- .entry(font.clone())
+ .entry(font_id)
.or_insert_with(|| self.platform_text_system.font_metrics(font_id));
Ok(read(metrics))
}
@@ -390,6 +376,7 @@ pub struct RenderGlyphParams {
pub(crate) font_size: Pixels,
pub(crate) subpixel_variant: Point<u8>,
pub(crate) scale_factor: f32,
+ pub(crate) is_emoji: bool,
}
impl Eq for RenderGlyphParams {}
@@ -109,77 +109,74 @@ impl Line {
let text_system = cx.text_system().clone();
for run in &self.layout.runs {
- text_system.with_font(run.font_id, |system, font| {
- let max_glyph_width = system.bounding_box(font, self.layout.font_size)?.size.width;
+ let max_glyph_width = text_system
+ .bounding_box(run.font_id, self.layout.font_size)?
+ .size
+ .width;
- for glyph in &run.glyphs {
- let glyph_origin = origin + baseline_offset + glyph.position;
- if glyph_origin.x > visible_bounds.upper_right().x {
- break;
- }
+ for glyph in &run.glyphs {
+ let glyph_origin = origin + baseline_offset + glyph.position;
+ if glyph_origin.x > visible_bounds.upper_right().x {
+ break;
+ }
- let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
- if glyph.index >= run_end {
- if let Some(style_run) = style_runs.next() {
- if let Some((_, underline_style)) = &mut underline {
- if style_run.underline != *underline_style {
- finished_underline = underline.take();
- }
- }
- if style_run.underline.thickness > px(0.) {
- underline.get_or_insert((
- point(
- glyph_origin.x,
- origin.y
- + baseline_offset.y
- + (self.layout.descent * 0.618),
- ),
- UnderlineStyle {
- color: style_run.underline.color,
- thickness: style_run.underline.thickness,
- squiggly: style_run.underline.squiggly,
- },
- ));
+ let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+ if glyph.index >= run_end {
+ if let Some(style_run) = style_runs.next() {
+ if let Some((_, underline_style)) = &mut underline {
+ if style_run.underline != *underline_style {
+ finished_underline = underline.take();
}
-
- run_end += style_run.len as usize;
- color = style_run.color;
- } else {
- run_end = self.layout.len;
- finished_underline = underline.take();
}
- }
+ if style_run.underline.thickness > px(0.) {
+ underline.get_or_insert((
+ point(
+ glyph_origin.x,
+ origin.y + baseline_offset.y + (self.layout.descent * 0.618),
+ ),
+ UnderlineStyle {
+ color: style_run.underline.color,
+ thickness: style_run.underline.thickness,
+ squiggly: style_run.underline.squiggly,
+ },
+ ));
+ }
- if glyph_origin.x + max_glyph_width < visible_bounds.origin.x {
- continue;
+ run_end += style_run.len as usize;
+ color = style_run.color;
+ } else {
+ run_end = self.layout.len;
+ finished_underline = underline.take();
}
+ }
- if let Some((_underline_origin, _underline_style)) = finished_underline {
- todo!()
- }
+ if glyph_origin.x + max_glyph_width < visible_bounds.origin.x {
+ continue;
+ }
- if glyph.is_emoji {
- cx.paint_emoji(
- glyph_origin,
- layout.order,
- run.font_id,
- glyph.id,
- self.layout.font_size,
- )?;
- } else {
- cx.paint_glyph(
- glyph_origin,
- layout.order,
- run.font_id,
- glyph.id,
- self.layout.font_size,
- color,
- )?;
- }
+ if let Some((_underline_origin, _underline_style)) = finished_underline {
+ todo!()
}
- anyhow::Ok(())
- })??;
+ if glyph.is_emoji {
+ cx.paint_emoji(
+ glyph_origin,
+ layout.order,
+ run.font_id,
+ glyph.id,
+ self.layout.font_size,
+ )?;
+ } else {
+ cx.paint_glyph(
+ glyph_origin,
+ layout.order,
+ run.font_id,
+ glyph.id,
+ self.layout.font_size,
+ color,
+ )?;
+ }
+ }
}
if let Some((_underline_start, _underline_style)) = underline.take() {
@@ -281,31 +278,31 @@ impl Line {
// });
}
- cx.text_system().with_font(run.font_id, |system, font| {
- let _glyph_bounds = Bounds {
- origin: glyph_origin,
- size: system.bounding_box(font, self.layout.font_size)?.size,
- };
- // if glyph_bounds.intersects(visible_bounds) {
- // if glyph.is_emoji {
- // cx.scene().push_image_glyph(scene::ImageGlyph {
- // font_id: run.font_id,
- // font_size: self.layout.font_size,
- // id: glyph.id,
- // origin: glyph_bounds.origin() + baseline_offset,
- // });
- // } else {
- // cx.scene().push_glyph(scene::Glyph {
- // font_id: run.font_id,
- // font_size: self.layout.font_size,
- // id: glyph.id,
- // origin: glyph_bounds.origin() + baseline_offset,
- // color,
- // });
- // }
- // }
- anyhow::Ok(())
- })??;
+ let text_system = cx.text_system();
+ let _glyph_bounds = Bounds {
+ origin: glyph_origin,
+ size: text_system
+ .bounding_box(run.font_id, self.layout.font_size)?
+ .size,
+ };
+ // if glyph_bounds.intersects(visible_bounds) {
+ // if glyph.is_emoji {
+ // cx.scene().push_image_glyph(scene::ImageGlyph {
+ // font_id: run.font_id,
+ // font_size: self.layout.font_size,
+ // id: glyph.id,
+ // origin: glyph_bounds.origin() + baseline_offset,
+ // });
+ // } else {
+ // cx.scene().push_glyph(scene::Glyph {
+ // font_id: run.font_id,
+ // font_size: self.layout.font_size,
+ // id: glyph.id,
+ // origin: glyph_bounds.origin() + baseline_offset,
+ // color,
+ // });
+ // }
+ // }
}
}
@@ -16,8 +16,7 @@ pub struct AnyWindow {}
pub struct Window {
handle: AnyWindowHandle,
platform_window: MainThreadOnly<Box<dyn PlatformWindow>>,
- monochrome_sprite_atlas: Arc<dyn PlatformAtlas>,
- polychrome_sprite_atlas: Arc<dyn PlatformAtlas>,
+ sprite_atlas: Arc<dyn PlatformAtlas>,
rem_size: Pixels,
content_size: Size<Pixels>,
layout_engine: TaffyLayoutEngine,
@@ -36,8 +35,7 @@ impl Window {
cx: &mut MainThread<AppContext>,
) -> Self {
let platform_window = cx.platform().open_window(handle, options);
- let monochrome_sprite_atlas = platform_window.monochrome_sprite_atlas();
- let polychrome_sprite_atlas = platform_window.polychrome_sprite_atlas();
+ let sprite_atlas = platform_window.sprite_atlas();
let mouse_position = platform_window.mouse_position();
let content_size = platform_window.content_size();
let scale_factor = platform_window.scale_factor();
@@ -60,8 +58,7 @@ impl Window {
Window {
handle,
platform_window,
- monochrome_sprite_atlas,
- polychrome_sprite_atlas,
+ sprite_atlas,
rem_size: px(16.),
content_size,
layout_engine: TaffyLayoutEngine::new(),
@@ -231,6 +228,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
font_size,
subpixel_variant,
scale_factor,
+ is_emoji: false,
};
let raster_bounds = self.text_system().raster_bounds(¶ms)?;
@@ -238,7 +236,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
let layer_id = self.current_layer_id();
let tile = self
.window
- .monochrome_sprite_atlas
+ .sprite_atlas
.get_or_insert_with(¶ms.clone().into(), &mut || {
self.text_system().rasterize_glyph(¶ms)
})?;
@@ -262,47 +260,6 @@ impl<'a, 'w> WindowContext<'a, 'w> {
Ok(())
}
- pub fn paint_svg(
- &mut self,
- bounds: Bounds<Pixels>,
- order: u32,
- path: SharedString,
- color: Hsla,
- ) -> Result<()> {
- let scale_factor = self.scale_factor();
- let bounds = bounds.scale(scale_factor);
- // Render the SVG at twice the size to get a higher quality result.
- let params = RenderSvgParams {
- path,
- size: bounds
- .size
- .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
- };
-
- let layer_id = self.current_layer_id();
- let tile = self.window.monochrome_sprite_atlas.get_or_insert_with(
- ¶ms.clone().into(),
- &mut || {
- let bytes = self.svg_renderer.render(¶ms)?;
- Ok((params.size, bytes))
- },
- )?;
- let content_mask = self.content_mask().scale(scale_factor);
-
- self.window.scene.insert(
- layer_id,
- MonochromeSprite {
- order,
- bounds,
- content_mask,
- color,
- tile,
- },
- );
-
- Ok(())
- }
-
pub fn paint_emoji(
&mut self,
origin: Point<Pixels>,
@@ -317,8 +274,10 @@ impl<'a, 'w> WindowContext<'a, 'w> {
font_id,
glyph_id,
font_size,
+ // We don't render emojis with subpixel variants.
subpixel_variant: Default::default(),
scale_factor,
+ is_emoji: true,
};
let raster_bounds = self.text_system().raster_bounds(¶ms)?;
@@ -326,7 +285,7 @@ impl<'a, 'w> WindowContext<'a, 'w> {
let layer_id = self.current_layer_id();
let tile = self
.window
- .polychrome_sprite_atlas
+ .sprite_atlas
.get_or_insert_with(¶ms.clone().into(), &mut || {
self.text_system().rasterize_glyph(¶ms)
})?;
@@ -349,6 +308,47 @@ impl<'a, 'w> WindowContext<'a, 'w> {
Ok(())
}
+ pub fn paint_svg(
+ &mut self,
+ bounds: Bounds<Pixels>,
+ order: u32,
+ path: SharedString,
+ color: Hsla,
+ ) -> Result<()> {
+ let scale_factor = self.scale_factor();
+ let bounds = bounds.scale(scale_factor);
+ // Render the SVG at twice the size to get a higher quality result.
+ let params = RenderSvgParams {
+ path,
+ size: bounds
+ .size
+ .map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
+ };
+
+ let layer_id = self.current_layer_id();
+ let tile =
+ self.window
+ .sprite_atlas
+ .get_or_insert_with(¶ms.clone().into(), &mut || {
+ let bytes = self.svg_renderer.render(¶ms)?;
+ Ok((params.size, bytes))
+ })?;
+ let content_mask = self.content_mask().scale(scale_factor);
+
+ self.window.scene.insert(
+ layer_id,
+ MonochromeSprite {
+ order,
+ bounds,
+ content_mask,
+ color,
+ tile,
+ },
+ );
+
+ Ok(())
+ }
+
pub(crate) fn draw(&mut self) -> Result<()> {
let unit_entity = self.unit_entity.clone();
self.update_entity(&unit_entity, |_, cx| {
@@ -51,7 +51,7 @@ impl CollabPanel {
//:: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
// .group()
// List Section Header
- .child(self.list_section_header("#CRDB", true, theme))
+ .child(self.list_section_header("#CRDB 🗃️", true, theme))
// List Item Large
.child(self.list_item(
"http://github.com/maxbrunsfeld.png?s=50",