diff --git a/examples/scrolling.rs b/examples/scrolling.rs index c4c23871bbcb5d7153edafebf6531b9250585c11..078f3ce7737677dc674241e7cb787984a24edde8 100644 --- a/examples/scrolling.rs +++ b/examples/scrolling.rs @@ -6,7 +6,13 @@ fn startup( mut font_mapping: ResMut<FontMapping>, asset_server: Res<AssetServer>, ) { - font_mapping.set_default(asset_server.load("roboto.kayak_font")); + let font_asset = asset_server.load("roboto.kayak_font"); + font_mapping.set_default(font_asset.clone()); + + // You can force the entire font to use subpixel rendering. + // Note: The subpixel settings on the text widget or render command + // will be ignored if this setting is used. + font_mapping.force_subpixel(&font_asset); // Camera 2D forces a clear pass in bevy. // We do this because our scene is not rendering anything else. diff --git a/examples/text.rs b/examples/text.rs index 142c89d9c8fe4a7126932630f76738f721e2c32a..325c756327ed5362c0f225e7ab12c4b1c2232622 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -21,6 +21,7 @@ fn my_widget_1_render( content: format!("My number is: {}", my_widget.foo), alignment: Alignment::Start, word_wrap: false, + subpixel: false, }), ..Default::default() } diff --git a/examples/vec.rs b/examples/vec.rs index adb28c31ac3fc3ca5d17d9a1f2db5d776d898b72..98c61427e034c1502b1cd5627c560046efb03dc3 100644 --- a/examples/vec.rs +++ b/examples/vec.rs @@ -1,5 +1,5 @@ use bevy::prelude::*; -use kayak_ui::prelude::{widgets::*, KStyle, *}; +use kayak_ui::prelude::{widgets::*, *}; #[derive(Component, Default, PartialEq, Eq, Clone)] pub struct MyWidgetProps {} diff --git a/src/render/font/extract.rs b/src/render/font/extract.rs index e6051ccf5b7c580325259c9dc4973a65faa5676e..bba0488b75708df768d0621e574348f25e1b95f5 100644 --- a/src/render/font/extract.rs +++ b/src/render/font/extract.rs @@ -20,15 +20,17 @@ pub fn extract_texts( _dpi: f32, ) -> Vec<ExtractQuadBundle> { let mut extracted_texts = Vec::new(); - let (background_color, text_layout, layout, font, properties) = match render_primitive { + let (background_color, text_layout, layout, font, properties, subpixel) = match render_primitive + { RenderPrimitive::Text { color, text_layout, layout, font, properties, + subpixel, .. - } => (color, text_layout, layout, font, *properties), + } => (color, text_layout, layout, font, *properties, subpixel), _ => panic!(""), }; @@ -40,6 +42,8 @@ pub fn extract_texts( } }; + let forced = font_mapping.get_subpixel_forced(&font_handle); + let base_position = Vec2::new(layout.posx, layout.posy + properties.font_size); for glyph_rect in text_layout.glyphs() { @@ -60,7 +64,11 @@ pub fn extract_texts( vertex_index: 0, char_id: font.get_char_id(glyph_rect.content).unwrap(), z_index: layout.z_index, - quad_type: UIQuadType::Text, + quad_type: if *subpixel || forced { + UIQuadType::TextSubpixel + } else { + UIQuadType::Text + }, type_index: 0, border_radius: Corner::default(), image: None, diff --git a/src/render/font/font_mapping.rs b/src/render/font/font_mapping.rs index df015c620ed19324f512cd2c7078a6218a42fd5b..bfd013416f54a760dc45a76ede8a450c565c921c 100644 --- a/src/render/font/font_mapping.rs +++ b/src/render/font/font_mapping.rs @@ -1,92 +1,107 @@ -use bevy::{ - prelude::{Handle, Resource}, - utils::HashMap, -}; -use kayak_font::KayakFont; - -// use crate::context::Context; - -/// A resource used to manage fonts for use in a `KayakContext` -/// -/// # Example -/// -/// ``` -/// use bevy::prelude::*; -/// use bevy_kayak_ui::FontMapping; -/// -/// fn setup_ui( -/// # mut commands: Commands, -/// asset_server: Res<AssetServer>, -/// mut font_mapping: ResMut<FontMapping> -/// ) { -/// # commands.spawn_bundle(UICameraBundle::new()); -/// # -/// font_mapping.set_default(asset_server.load("roboto.kayak_font")); -/// // ... -/// # -/// # let context = BevyContext::new(|context| { -/// # render! { -/// # <App> -/// # <Text content={"Hello World!".to_string()} /> -/// # </App> -/// # } -/// # }); -/// # -/// # commands.insert_resource(context); -/// } -/// ``` -#[derive(Resource, Default)] -pub struct FontMapping { - font_ids: HashMap<Handle<KayakFont>, String>, - font_handles: HashMap<String, Handle<KayakFont>>, - new_fonts: Vec<String>, -} - -impl FontMapping { - /// Add a `KayakFont` to be tracked - pub fn add(&mut self, key: impl Into<String>, handle: Handle<KayakFont>) { - let key = key.into(); - if !self.font_ids.contains_key(&handle) { - self.font_ids.insert(handle.clone(), key.clone()); - self.new_fonts.push(key.clone()); - self.font_handles.insert(key, handle); - } - } - - /// Set a default `KayakFont` - pub fn set_default(&mut self, handle: Handle<KayakFont>) { - self.add(crate::DEFAULT_FONT, handle); - } - - pub(crate) fn mark_all_as_new(&mut self) { - self.new_fonts.extend(self.font_handles.keys().cloned()); - } - - /// Get the handle for the given font name - pub fn get_handle(&self, id: String) -> Option<Handle<KayakFont>> { - self.font_handles.get(&id).cloned() - } - - /// Get the font name for the given handle - pub fn get(&self, font: &Handle<KayakFont>) -> Option<String> { - self.font_ids.get(font).cloned() - } - - // pub(crate) fn add_loaded_to_kayak( - // &mut self, - // fonts: &Res<Assets<KayakFont>>, - // context: &Context, - // ) { - // if let Ok(mut kayak_context) = context.kayak_context.write() { - // let new_fonts = self.new_fonts.drain(..).collect::<Vec<_>>(); - // for font_key in new_fonts { - // let font_handle = self.font_handles.get(&font_key).unwrap(); - // if let Some(font) = fonts.get(font_handle) { - // kayak_context.set_asset(font_key, font.clone()); - // } else { - // self.new_fonts.push(font_key); - // } - // } - // } - // } -} +use bevy::{ + prelude::{Handle, Resource}, + utils::{HashMap, HashSet}, +}; +use kayak_font::KayakFont; + +// use crate::context::Context; + +/// A resource used to manage fonts for use in a `KayakContext` +/// +/// # Example +/// +/// ``` +/// use bevy::prelude::*; +/// use bevy_kayak_ui::FontMapping; +/// +/// fn setup_ui( +/// # mut commands: Commands, +/// asset_server: Res<AssetServer>, +/// mut font_mapping: ResMut<FontMapping> +/// ) { +/// # commands.spawn_bundle(UICameraBundle::new()); +/// # +/// font_mapping.set_default(asset_server.load("roboto.kayak_font")); +/// // ... +/// # +/// # let context = BevyContext::new(|context| { +/// # render! { +/// # <App> +/// # <Text content={"Hello World!".to_string()} /> +/// # </App> +/// # } +/// # }); +/// # +/// # commands.insert_resource(context); +/// } +/// ``` +#[derive(Resource, Default)] +pub struct FontMapping { + font_ids: HashMap<Handle<KayakFont>, String>, + font_handles: HashMap<String, Handle<KayakFont>>, + new_fonts: Vec<String>, + subpixel: HashSet<Handle<KayakFont>>, +} + +impl FontMapping { + /// Add a `KayakFont` to be tracked + pub fn add(&mut self, key: impl Into<String>, handle: Handle<KayakFont>) { + let key = key.into(); + if !self.font_ids.contains_key(&handle) { + self.font_ids.insert(handle.clone(), key.clone()); + self.new_fonts.push(key.clone()); + self.font_handles.insert(key, handle); + } + } + + /// Set a default `KayakFont` + pub fn set_default(&mut self, handle: Handle<KayakFont>) { + self.add(crate::DEFAULT_FONT, handle); + } + + pub(crate) fn mark_all_as_new(&mut self) { + self.new_fonts.extend(self.font_handles.keys().cloned()); + } + + /// Get the handle for the given font name + pub fn get_handle(&self, id: String) -> Option<Handle<KayakFont>> { + self.font_handles.get(&id).cloned() + } + + /// Get the font name for the given handle + pub fn get(&self, font: &Handle<KayakFont>) -> Option<String> { + self.font_ids.get(font).cloned() + } + + /// Forces any text render commands to use subpixel font rendering for this specific font asset. + pub fn force_subpixel(&mut self, font: &Handle<KayakFont>) { + self.subpixel.insert(font.clone_weak()); + } + + /// Turns off the forced subpixel rendering mode for this font. + pub fn disable_subpixel(&mut self, font: &Handle<KayakFont>) { + self.subpixel.remove(font); + } + + pub fn get_subpixel_forced(&self, font: &Handle<KayakFont>) -> bool { + self.subpixel.contains(font) + } + + // pub(crate) fn add_loaded_to_kayak( + // &mut self, + // fonts: &Res<Assets<KayakFont>>, + // context: &Context, + // ) { + // if let Ok(mut kayak_context) = context.kayak_context.write() { + // let new_fonts = self.new_fonts.drain(..).collect::<Vec<_>>(); + // for font_key in new_fonts { + // let font_handle = self.font_handles.get(&font_key).unwrap(); + // if let Some(font) = fonts.get(font_handle) { + // kayak_context.set_asset(font_key, font.clone()); + // } else { + // self.new_fonts.push(font_key); + // } + // } + // } + // } +} diff --git a/src/render/unified/pipeline.rs b/src/render/unified/pipeline.rs index d8a71309ee8d32b1f574106dfd0a401a57f3d9a2..4d5aa330e4da2f9191d55a252257e925a26ee7c8 100644 --- a/src/render/unified/pipeline.rs +++ b/src/render/unified/pipeline.rs @@ -333,6 +333,7 @@ pub struct ExtractQuadBundle { pub enum UIQuadType { Quad, Text, + TextSubpixel, Image, Clip, } @@ -416,18 +417,24 @@ pub fn prepare_quads( _padding_2: 0, _padding_3: 0, }); - let text_type_offset = sprite_meta.types_buffer.push(QuadType { + let text_sub_pixel_type_offset = sprite_meta.types_buffer.push(QuadType { t: 1, _padding_1: 0, _padding_2: 0, _padding_3: 0, }); - let image_type_offset = sprite_meta.types_buffer.push(QuadType { + let text_type_offset = sprite_meta.types_buffer.push(QuadType { t: 2, _padding_1: 0, _padding_2: 0, _padding_3: 0, }); + let image_type_offset = sprite_meta.types_buffer.push(QuadType { + t: 3, + _padding_1: 0, + _padding_2: 0, + _padding_3: 0, + }); sprite_meta .types_buffer @@ -450,6 +457,7 @@ pub fn prepare_quads( match extracted_sprite.quad_type { UIQuadType::Quad => extracted_sprite.type_index = quad_type_offset, UIQuadType::Text => extracted_sprite.type_index = text_type_offset, + UIQuadType::TextSubpixel => extracted_sprite.type_index = text_sub_pixel_type_offset, UIQuadType::Image => extracted_sprite.type_index = image_type_offset, UIQuadType::Clip => {} }; diff --git a/src/render/unified/shader.wgsl b/src/render/unified/shader.wgsl index d68144ae03072c5a50b2d0f19db093084ecd4d3e..5bf6e13a5bc6b5263852574fab31919198f1e19a 100644 --- a/src/render/unified/shader.wgsl +++ b/src/render/unified/shader.wgsl @@ -110,6 +110,16 @@ fn fragment(in: VertexOutput) -> @location(0) vec4<f32> { return vec4(red * in.color.r, green * in.color.g, blue * in.color.b, alpha); } if quad_type.t == 2 { + var px_range = 2.5; + var tex_dimensions = textureDimensions(font_texture); + var msdf_unit = vec2<f32>(px_range, px_range) / vec2<f32>(f32(tex_dimensions.x), f32(tex_dimensions.y)); + var x = textureSample(font_texture, font_sampler, vec2<f32>(in.uv.x, 1.0 - in.uv.y), i32(in.uv.z)); + var v = max(min(x.r, x.g), min(max(x.r, x.g), x.b)); + var sig_dist = (v - 0.5) * dot(msdf_unit, 0.5 / fwidth(vec2<f32>(in.uv.x, 1.0 - in.uv.y))); + var a = clamp(sig_dist + 0.5, 0.0, 1.0); + return vec4<f32>(in.color.rgb, a); + } + if quad_type.t == 3 { var bs = min(in.border_radius, min(in.size.x, in.size.y)); var mask = sdRoundBox( in.pos.xy * 2.0 - (in.size.xy), diff --git a/src/render_primitive.rs b/src/render_primitive.rs index 9c3fa2d6820b8bc656199be97f0ffffe9d9e30aa..89835a8ea481c7e38c440a42e830e3870efeb2fc 100644 --- a/src/render_primitive.rs +++ b/src/render_primitive.rs @@ -29,6 +29,7 @@ pub enum RenderPrimitive { layout: Rect, properties: TextProperties, word_wrap: bool, + subpixel: bool, }, Image { border_radius: Corner<f32>, @@ -111,6 +112,7 @@ impl From<&KStyle> for RenderPrimitive { content, alignment, word_wrap, + subpixel, } => Self::Text { color: style.color.resolve(), content, @@ -124,6 +126,7 @@ impl From<&KStyle> for RenderPrimitive { ..Default::default() }, word_wrap, + subpixel, }, RenderCommand::Image { handle } => Self::Image { border_radius: style.border_radius.resolve(), diff --git a/src/styles/render_command.rs b/src/styles/render_command.rs index d5a2630214bf9738b15858a23017f631f7c9110c..7583648da8fbec097b789cfe9dc5f591f3948c02 100644 --- a/src/styles/render_command.rs +++ b/src/styles/render_command.rs @@ -17,6 +17,7 @@ pub enum RenderCommand { content: String, alignment: Alignment, word_wrap: bool, + subpixel: bool, }, Image { handle: Handle<Image>, diff --git a/src/widgets/text.rs b/src/widgets/text.rs index db20b9ca4b22c7a4ac7c14d528f1984b2319c274..6de78c40f61be608c9e8122cdbd801e83d27ac74 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -31,6 +31,8 @@ pub struct TextProps { /// Basic word wrapping. /// Defautls to true pub word_wrap: bool, + /// Enables subpixel rendering of text. This is useful on smaller low-dpi screens. + pub subpixel: bool, } impl Default for TextProps { @@ -43,6 +45,7 @@ impl Default for TextProps { size: -1.0, alignment: Alignment::Start, word_wrap: true, + subpixel: false, } } } @@ -82,6 +85,7 @@ pub fn text_render( content: text.content.clone(), alignment: text.alignment, word_wrap: text.word_wrap, + subpixel: text.subpixel, }), font: if let Some(ref font) = text.font { StyleProp::Value(font.clone())