diff --git a/Cargo.lock b/Cargo.lock index 308bd241e1cde964e4758c9ec9ceb52d9903ef4a..a81e041111e8bcbad0164627fa425d43d2b3c2d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,7 +388,7 @@ dependencies = [ [[package]] name = "bevy_cosmic_edit" -version = "0.13.0" +version = "0.14.0" dependencies = [ "arboard", "bevy", diff --git a/Cargo.toml b/Cargo.toml index 781c2dcf5cada8d86bd8d224359f59b23aa705a3..5fb3fba90322beeca5aab26566ea5ea76265a089 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_cosmic_edit" -version = "0.13.0" +version = "0.14.0" edition = "2021" license = "MIT OR Apache-2.0" description = "Bevy cosmic-text multiline text input" @@ -12,6 +12,13 @@ exclude = ["assets/*"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# Enable max optimizations for dependencies, but not for our code: +[profile.dev.package."*"] +opt-level = 3 + +[profile.release] +opt-level = 'z' + [dependencies] bevy = { version = "0.11", default-features = false, features = [ "bevy_asset", @@ -39,16 +46,4 @@ js-sys = "0.3.61" insta = "1.29.0" [[example]] -name = "basic_ui" - -[[example]] -name = "basic_sprite" - -[[example]] -name = "font_per_widget" - -[[example]] -name = "multiple_sprites" - -[[example]] -name = "readonly" +name = "text_input" \ No newline at end of file diff --git a/examples/every_option.rs b/examples/every_option.rs index f09282f3dc5547a52b62d7a9f427ff3bac5f7a2b..e1c4ebdecec860908da83215c5c5a28d077c7367 100644 --- a/examples/every_option.rs +++ b/examples/every_option.rs @@ -45,6 +45,7 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { max_chars: CosmicMaxChars(15), max_lines: CosmicMaxLines(1), text: CosmicText::OneStyle("BANANA IS THE CODEWORD!".into()), + mode: CosmicMode::Wrap, }) .id(); @@ -84,7 +85,7 @@ fn text_swapper( } let editor = editor_q.single(); - println!("X OFFSET: {}", get_x_offset(editor.0.buffer())); + println!("X OFFSET: {}", get_x_offset_center(editor.0.buffer())); } fn main() { diff --git a/examples/restricted_input.rs b/examples/text_input.rs similarity index 85% rename from examples/restricted_input.rs rename to examples/text_input.rs index 65b93fc8dd771558cbe51a529d6fe2b6c6bc158b..d9e0807a348a0add16abd5576dd196cfc61cd867 100644 --- a/examples/restricted_input.rs +++ b/examples/text_input.rs @@ -25,13 +25,10 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { line_height: 16., scale_factor: primary_window.scale_factor() as f32, }, - max_chars: CosmicMaxChars(23), max_lines: CosmicMaxLines(1), - text: CosmicText::OneStyle( - "1 line 25 chars! But this a b c d e f g\n is longer\n than is\n allowed by\n the limits.\n" - .into(), - ), + text: CosmicText::OneStyle("".into()), text_position: CosmicTextPosition::Left { padding: 20 }, + mode: CosmicMode::InfiniteLine, ..default() }) .id(); diff --git a/readme.md b/readme.md index 9b051488b9782e96ef92b1c427098317e68f1ffc..c46682bcbc5f1354eb8ce9c7469f1c6b1bace0c1 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,7 @@ Explore examples folder for basic usage. Native: ```rust -cargo r --example restricted_input +cargo r --example text_input ``` Wasm: diff --git a/src/lib.rs b/src/lib.rs index a1b1d43e4f4377a56dba14f9b94d9d56dbb832ec..7e60577ae41dbfcee69855b9b777bda469ab6df6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ pub use cosmic_text::{ Weight as FontWeight, }; use cosmic_text::{ - AttrsList, Buffer, BufferLine, Editor, FontSystem, Metrics, Shaping, SwashCache, + Affinity, AttrsList, Buffer, BufferLine, Editor, FontSystem, Metrics, Shaping, SwashCache, }; use image::{imageops::FilterType, GenericImageView}; @@ -25,6 +25,14 @@ pub enum CosmicText { MultiStyle(Vec<Vec<(String, AttrsOwned)>>), } +#[derive(Clone, Component, PartialEq, Default)] +pub enum CosmicMode { + InfiniteLine, + AutoHeight, + #[default] + Wrap, +} + impl Default for CosmicText { fn default() -> Self { Self::OneStyle(String::new()) @@ -71,6 +79,9 @@ pub struct CosmicFontSystem(pub FontSystem); #[derive(Component)] pub struct ReadOnly; // tag component +#[derive(Component, Debug)] +struct XOffset(Option<(f32, f32)>); + #[derive(Component)] pub struct CosmicEditor(pub Editor); @@ -172,10 +183,12 @@ fn cosmic_editor_builder( for (entity, text, attrs, metrics, max_chars, max_lines, readonly, node, sprite) in added_editors.iter_mut() { - let mut editor = Editor::new(Buffer::new( + let buffer = Buffer::new( &mut font_system.0, Metrics::new(metrics.font_size, metrics.line_height).scale(metrics.scale_factor), - )); + ); + // buffer.set_wrap(&mut font_system.0, cosmic_text::Wrap::None); + let mut editor = Editor::new(buffer); if let Some(node) = node { editor @@ -208,6 +221,7 @@ fn cosmic_editor_builder( // Add edit history commands.entity(entity).insert(CosmicEditHistory::default()); + commands.entity(entity).insert(XOffset(None)); } } @@ -300,6 +314,8 @@ pub struct CosmicEditUiBundle { pub max_chars: CosmicMaxChars, /// Setting this will update the buffer's text pub text: CosmicText, + /// Text input mode + pub mode: CosmicMode, } impl Default for CosmicEditUiBundle { @@ -325,6 +341,7 @@ impl Default for CosmicEditUiBundle { max_lines: Default::default(), max_chars: Default::default(), text: Default::default(), + mode: Default::default(), } } } @@ -357,6 +374,8 @@ pub struct CosmicEditSpriteBundle { pub max_chars: CosmicMaxChars, /// Setting this will update the buffer's text pub text: CosmicText, + /// Text input mode + pub mode: CosmicMode, } impl Default for CosmicEditSpriteBundle { @@ -376,6 +395,7 @@ impl Default for CosmicEditSpriteBundle { max_lines: Default::default(), max_chars: Default::default(), text: Default::default(), + mode: Default::default(), } } } @@ -469,7 +489,9 @@ fn trim_text(text: CosmicText, max_chars: usize, max_lines: usize) -> CosmicText match text { CosmicText::OneStyle(mut string) => { - string.truncate(max_chars); + if max_chars != 0 { + string.truncate(max_chars); + } if max_lines == 0 { return CosmicText::OneStyle(string); @@ -686,12 +708,12 @@ fn get_text_size(buffer: &Buffer) -> (f32, f32) { (width.unwrap(), height) } -pub fn get_y_offset(buffer: &Buffer) -> i32 { +pub fn get_y_offset_center(buffer: &Buffer) -> i32 { let (_, text_height) = get_text_size(buffer); ((buffer.size().1 - text_height) / 2.0) as i32 } -pub fn get_x_offset(buffer: &Buffer) -> i32 { +pub fn get_x_offset_center(buffer: &Buffer) -> i32 { let (text_width, _) = get_text_size(buffer); ((buffer.size().0 - text_width) / 2.0) as i32 } @@ -714,6 +736,7 @@ fn cosmic_edit_bevy_events( &CosmicMaxLines, &CosmicMaxChars, Entity, + &XOffset, ), With<CosmicEditor>, >, @@ -740,6 +763,7 @@ fn cosmic_edit_bevy_events( max_lines, max_chars, entity, + x_offset, ) in &mut cosmic_edit_query.iter_mut() { let readonly = readonly_query.get(entity).is_ok(); @@ -1014,18 +1038,20 @@ fn cosmic_edit_bevy_events( } } } - let (offset_x, offset_y) = match text_position { + let (padding_x, padding_y) = match text_position { CosmicTextPosition::Center => ( - get_x_offset(editor.0.buffer()), - get_y_offset(editor.0.buffer()), + get_x_offset_center(editor.0.buffer()), + get_y_offset_center(editor.0.buffer()), ), CosmicTextPosition::TopLeft { padding } => (*padding, *padding), - CosmicTextPosition::Left { padding } => (*padding, get_y_offset(editor.0.buffer())), + CosmicTextPosition::Left { padding } => { + (*padding, get_y_offset_center(editor.0.buffer())) + } }; let point = |node_cursor_pos: (f32, f32)| { ( - (node_cursor_pos.0 * scale_factor) as i32 - offset_x, - (node_cursor_pos.1 * scale_factor) as i32 - offset_y, + (node_cursor_pos.0 * scale_factor) as i32 - padding_x, + (node_cursor_pos.1 * scale_factor) as i32 - padding_y, ) }; @@ -1038,7 +1064,8 @@ fn cosmic_edit_bevy_events( camera, camera_transform, ) { - let (x, y) = point(node_cursor_pos); + let (mut x, y) = point(node_cursor_pos); + x += x_offset.0.unwrap_or((0., 0.)).0 as i32; if shift { editor.0.action(&mut font_system.0, Action::Drag { x, y }); } else { @@ -1056,7 +1083,8 @@ fn cosmic_edit_bevy_events( camera, camera_transform, ) { - let (x, y) = point(node_cursor_pos); + let (mut x, y) = point(node_cursor_pos); + x += x_offset.0.unwrap_or((0., 0.)).0 as i32; if active_editor.is_changed() && !shift { editor.0.action(&mut font_system.0, Action::Click { x, y }); } else { @@ -1154,6 +1182,8 @@ fn cosmic_edit_set_redraw(mut cosmic_edit_query: Query<&mut CosmicEditor, Added< #[allow(clippy::too_many_arguments)] fn redraw_buffer_common( + mode: &CosmicMode, + x_offset: &mut XOffset, images: &mut ResMut<Assets<Image>>, swash_cache_state: &mut ResMut<SwashCacheState>, editor: &mut Editor, @@ -1167,28 +1197,71 @@ fn redraw_buffer_common( original_width: f32, original_height: f32, ) { - let width = original_width * scale_factor; - let height = original_height * scale_factor; + let widget_width = original_width * scale_factor; + let widget_height = original_height * scale_factor; let swash_cache = &mut swash_cache_state.swash_cache; editor.shape_as_needed(&mut font_system.0); if editor.buffer().redraw() { + let mut cursor_x = 0.; + + if mode == &CosmicMode::InfiniteLine { + if let Some(line) = editor.buffer().layout_runs().next() { + for (idx, glyph) in line.glyphs.iter().enumerate() { + if editor.cursor().affinity == Affinity::Before { + if idx <= editor.cursor().index { + cursor_x += glyph.w; + } + } else if idx < editor.cursor().index { + cursor_x += glyph.w; + } else { + break; + } + } + } + } + + if mode == &CosmicMode::InfiniteLine && x_offset.0.is_none() && original_width > 1. { + let padding_x = match text_position { + CosmicTextPosition::Center => get_x_offset_center(editor.buffer()), + CosmicTextPosition::TopLeft { padding } => *padding, + CosmicTextPosition::Left { padding } => *padding, + }; + *x_offset = XOffset(Some((0., widget_width - 2. * padding_x as f32))); + } + + if let Some((x_min, x_max)) = x_offset.0 { + if cursor_x > x_max { + let diff = cursor_x - x_max; + *x_offset = XOffset(Some((x_min + diff, cursor_x))); + } + if cursor_x < x_min { + let diff = x_min - cursor_x; + *x_offset = XOffset(Some((cursor_x, x_max - diff))); + } + } + + let (buffer_width, buffer_height) = match mode { + CosmicMode::InfiniteLine => (f32::MAX, widget_height), + CosmicMode::AutoHeight => (widget_width, f32::MAX), + CosmicMode::Wrap => (widget_width, widget_height), + }; editor .buffer_mut() - .set_size(&mut font_system.0, width, height); + .set_size(&mut font_system.0, buffer_width, buffer_height); let font_color = attrs .0 .color_opt .unwrap_or(cosmic_text::Color::rgb(0, 0, 0)); - let mut pixels = vec![0; width as usize * height as usize * 4]; + let mut pixels = vec![0; widget_width as usize * widget_height as usize * 4]; if let Some(bg_image) = background_image { if let Some(image) = images.get(&bg_image) { let mut dynamic_image = image.clone().try_into_dynamic().unwrap(); - if image.size().x != width || image.size().y != height { + if image.size().x != widget_width || image.size().y != widget_height { dynamic_image = dynamic_image.resize_to_fill( - width as u32, - height as u32, + widget_width as u32, + widget_height as u32, FilterType::Triangle, ); } @@ -1210,13 +1283,15 @@ fn redraw_buffer_common( pixel[3] = (bg.a() * 255.) as u8; // Alpha component } } - - let (offset_y, offset_x) = match text_position { - CosmicTextPosition::Center => { - (get_y_offset(editor.buffer()), get_x_offset(editor.buffer())) - } + let (padding_y, padding_x) = match text_position { + CosmicTextPosition::Center => ( + get_y_offset_center(editor.buffer()), + get_x_offset_center(editor.buffer()), + ), CosmicTextPosition::TopLeft { padding } => (*padding, *padding), - CosmicTextPosition::Left { padding } => (get_y_offset(editor.buffer()), *padding), + CosmicTextPosition::Left { padding } => { + (get_y_offset_center(editor.buffer()), *padding) + } }; editor.draw( @@ -1228,10 +1303,10 @@ fn redraw_buffer_common( for col in 0..w as i32 { draw_pixel( &mut pixels, - width as i32, - height as i32, - x + col + offset_x, - y + row + offset_y, + widget_width as i32, + widget_height as i32, + x + col + padding_x - x_offset.0.unwrap_or((0., 0.)).0 as i32, + y + row + padding_y, color, ); } @@ -1246,8 +1321,8 @@ fn redraw_buffer_common( prev_image.data.clear(); prev_image.data.extend_from_slice(pixels.as_slice()); prev_image.resize(Extent3d { - width: width as u32, - height: height as u32, + width: widget_width as u32, + height: widget_height as u32, depth_or_array_layers: 1, }); let handle_id: HandleId = HandleId::random::<Image>(); @@ -1258,8 +1333,8 @@ fn redraw_buffer_common( prev_image.data.clear(); prev_image.data.extend_from_slice(pixels.as_slice()); prev_image.resize(Extent3d { - width: width as u32, - height: height as u32, + width: widget_width as u32, + height: widget_height as u32, depth_or_array_layers: 1, }); } @@ -1280,6 +1355,8 @@ fn cosmic_edit_redraw_buffer_ui( &mut UiImage, &Node, &mut Visibility, + &mut XOffset, + &CosmicMode, )>, mut font_system: ResMut<CosmicFontSystem>, ) { @@ -1293,6 +1370,8 @@ fn cosmic_edit_redraw_buffer_ui( mut img, node, mut visibility, + mut x_offset, + mode, ) in &mut cosmic_edit_query.iter_mut() { // provide min sizes to prevent render panic @@ -1300,6 +1379,8 @@ fn cosmic_edit_redraw_buffer_ui( let height = node.size().y.max(1.); redraw_buffer_common( + mode, + &mut x_offset, &mut images, &mut swash_cache_state, &mut editor.0, @@ -1440,6 +1521,8 @@ fn cosmic_edit_redraw_buffer( &CosmicTextPosition, &mut Handle<Image>, &mut Visibility, + &mut XOffset, + &CosmicMode, )>, mut font_system: ResMut<CosmicFontSystem>, ) { @@ -1453,6 +1536,8 @@ fn cosmic_edit_redraw_buffer( text_position, mut handle, mut visibility, + mut x_offset, + mode, ) in &mut cosmic_edit_query.iter_mut() { // provide min sizes to prevent render panic @@ -1460,6 +1545,8 @@ fn cosmic_edit_redraw_buffer( let height = sprite.custom_size.unwrap().y.max(1.); redraw_buffer_common( + mode, + &mut x_offset, &mut images, &mut swash_cache_state, &mut editor.0,