diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..608db70d773ee717187a3e5c3395b942ef251279 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## Version 0.17.0 (2024) + +- Update to cosmic-text 0.11.2 +- Drop a lot of functionalities like placeholders, password fields, undo/redo, etc to keep small core. All functionalities can be restored using internal plugins if users demand (PRs are welcome) diff --git a/Cargo.lock b/Cargo.lock index 6083f8c811df97a0b491166c5596e7e6558051ff..7622718f8802d485f2ec8b119f81db720bcc886c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,7 +406,7 @@ dependencies = [ [[package]] name = "bevy_cosmic_edit" -version = "0.16.0" +version = "0.17.0" dependencies = [ "arboard", "bevy", @@ -416,6 +416,7 @@ dependencies = [ "insta", "js-sys", "sys-locale", + "util", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1323,10 +1324,11 @@ dependencies = [ [[package]] name = "cosmic-text" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71" +checksum = "c578f2b9abb4d5f3fbb12aba4008084d435dc6a8425c195cfe0b3594bfea0c25" dependencies = [ + "bitflags 2.4.2", "fontdb", "libm", "log", @@ -1336,6 +1338,7 @@ dependencies = [ "self_cell", "swash", "sys-locale", + "ttf-parser", "unicode-bidi", "unicode-linebreak", "unicode-script", @@ -1640,16 +1643,16 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.15.0" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020e203f177c0fb250fb19455a252e838d2bbbce1f80f25ecc42402aafa8cd38" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" dependencies = [ "fontconfig-parser", "log", "memmap2", "slotmap", "tinyvec", - "ttf-parser 0.19.2", + "ttf-parser", ] [[package]] @@ -2137,9 +2140,9 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memmap2" -version = "0.8.0" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] @@ -2409,7 +2412,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4586edfe4c648c71797a74c84bacb32b52b212eff5dfe2bb9f2c599844023e7" dependencies = [ - "ttf-parser 0.20.0", + "ttf-parser", ] [[package]] @@ -2736,15 +2739,15 @@ dependencies = [ [[package]] name = "rustybuzz" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee8fe2a8461a0854a37101fe7a1b13998d0cfa987e43248e81d2a5f4570f6fa" +checksum = "f0ae5692c5beaad6a9e22830deeed7874eae8a4e3ba4076fb48e12c56856222c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "bytemuck", "libm", "smallvec", - "ttf-parser 0.20.0", + "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", "unicode-properties", @@ -3112,12 +3115,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ttf-parser" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" - [[package]] name = "ttf-parser" version = "0.20.0" @@ -3184,6 +3181,14 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "util" +version = "0.1.0" +dependencies = [ + "bevy", + "bevy_cosmic_edit", +] + [[package]] name = "uuid" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index ef8be70a87bebbc3b01ebfeb2c934ce0d3c7bb3c..c47d624d13b835c3da7c01022953ba56b2733acf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ +workspace = { members = ["util"] } [package] name = "bevy_cosmic_edit" -version = "0.16.0" +version = "0.17.0" edition = "2021" license = "MIT OR Apache-2.0" description = "Bevy cosmic-text multiline text input" @@ -29,7 +30,7 @@ bevy = { version = "0.13", default-features = false, features = [ "x11", "webgpu", ] } -cosmic-text = { version = "0.10" } +cosmic-text = { version = "0.11.2" } # TODO: move crossbeam to wasm32, once input.rs has separate wasm copy/paste fn crossbeam-channel = "0.5.8" image = "0.24.6" @@ -46,6 +47,4 @@ web-sys = { version = "0.3.67", features = ["Clipboard", "Navigator", "Window"] [dev-dependencies] insta = "1.29.0" - -[[example]] -name = "text_input" +util = {path="./util/"} diff --git a/examples/basic_sprite.rs b/examples/basic_sprite.rs index 3b560cd81e11393f45c372e2a9ac0329aed70b8d..bfdbefb28115ce1a89a059afc05b7ba2c01c572a 100644 --- a/examples/basic_sprite.rs +++ b/examples/basic_sprite.rs @@ -1,7 +1,12 @@ use bevy::{prelude::*, window::PrimaryWindow}; use bevy_cosmic_edit::*; +use util::{change_active_editor_sprite, deselect_editor_on_esc, print_editor_text}; -fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { +fn setup( + mut commands: Commands, + windows: Query<&Window, With<PrimaryWindow>>, + mut font_system: ResMut<CosmicFontSystem>, +) { let primary_window = windows.single(); let camera_bundle = Camera2dBundle { camera: Camera { @@ -16,17 +21,13 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { attrs = attrs.family(Family::Name("Victor Mono")); attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3)); - let scale_factor = primary_window.scale_factor() as f32; - let cosmic_edit = (CosmicEditBundle { - metrics: CosmicMetrics { - font_size: 14., - line_height: 18., - scale_factor, - }, + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text( + &mut font_system, + "馃榾馃榾馃榾 x => y", + attrs, + ), text_position: CosmicTextPosition::Center, - attrs: CosmicAttrs(AttrsOwned::new(attrs)), - text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y".to_string()), sprite_bundle: SpriteBundle { sprite: Sprite { custom_size: Some(Vec2::new(primary_window.width(), primary_window.height())), @@ -39,7 +40,7 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { let cosmic_edit = commands.spawn(cosmic_edit).id(); - commands.insert_resource(Focus(Some(cosmic_edit))); + commands.insert_resource(FocusedWidget(Some(cosmic_edit))); } fn main() { @@ -57,5 +58,13 @@ fn main() { ..default() }) .add_systems(Startup, setup) + .add_systems( + Update, + ( + print_editor_text, + change_active_editor_sprite, + deselect_editor_on_esc, + ), + ) .run(); } diff --git a/examples/basic_ui.rs b/examples/basic_ui.rs index 9679eddfbcf66138d18f9b20b2a1b363f6fc72a9..2584d4c57c19414a58f3c2e64d08bf754d66acbe 100644 --- a/examples/basic_ui.rs +++ b/examples/basic_ui.rs @@ -1,11 +1,11 @@ -use bevy::{prelude::*, window::PrimaryWindow}; +use bevy::prelude::*; use bevy_cosmic_edit::*; +use util::{change_active_editor_ui, deselect_editor_on_esc, print_editor_text}; -fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { - let primary_window = windows.single(); +fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) { let camera_bundle = Camera2dBundle { camera: Camera { - clear_color: ClearColorConfig::Custom(Color::WHITE), + clear_color: ClearColorConfig::Custom(Color::PINK), ..default() }, ..default() @@ -16,20 +16,16 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { attrs = attrs.family(Family::Name("Victor Mono")); attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3)); - let scale_factor = primary_window.scale_factor() as f32; - let cosmic_edit = commands - .spawn(CosmicEditBundle { - metrics: CosmicMetrics { - font_size: 14., - line_height: 18., - scale_factor, - }, + .spawn((CosmicEditBundle { + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(20., 20.)).with_rich_text( + &mut font_system, + vec![("Banana", attrs)], + attrs, + ), text_position: CosmicTextPosition::Center, - attrs: CosmicAttrs(AttrsOwned::new(attrs)), - text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y".to_string()), ..default() - }) + },)) .id(); commands @@ -42,8 +38,6 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { height: Val::Percent(100.), ..default() }, - // Needs to be set to prevent a bug where nothing is displayed - background_color: Color::WHITE.into(), ..default() }, ) @@ -52,21 +46,7 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { // texture to the editor's rendered image .insert(CosmicSource(cosmic_edit)); - commands.insert_resource(Focus(Some(cosmic_edit))); -} - -fn print_text( - text_inputs_q: Query<&CosmicEditor, With<CosmicEditor>>, - mut previous_value: Local<String>, -) { - for text_input in text_inputs_q.iter() { - let current_text = text_input.get_text(); - if current_text == *previous_value { - return; - } - *previous_value = current_text.clone(); - info!("Widget text: {}", current_text); - } + commands.insert_resource(FocusedWidget(Some(cosmic_edit))); } fn main() { @@ -84,6 +64,13 @@ fn main() { ..default() }) .add_systems(Startup, setup) - .add_systems(Update, print_text) + .add_systems( + Update, + ( + print_editor_text, + change_active_editor_ui, + deselect_editor_on_esc, + ), + ) .run(); } diff --git a/examples/bevy_api_testing.rs b/examples/bevy_api_testing.rs deleted file mode 100644 index 3f72e5ae84f75a85534f6c30a71bb20e0568d9ca..0000000000000000000000000000000000000000 --- a/examples/bevy_api_testing.rs +++ /dev/null @@ -1,142 +0,0 @@ -use bevy::{prelude::*, window::PrimaryWindow}; -use bevy_cosmic_edit::*; - -fn setup(mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); - - // UI editor - let ui_editor = commands - .spawn(CosmicEditBundle { - attrs: CosmicAttrs(AttrsOwned::new( - Attrs::new().color(bevy_color_to_cosmic(Color::GREEN)), - )), - max_lines: CosmicMaxLines(1), - ..default() - }) - .insert(CosmicEditPlaceholderBundle { - text_setter: PlaceholderText(CosmicText::OneStyle("Placeholder".into())), - attrs: PlaceholderAttrs(AttrsOwned::new( - Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))), - )), - }) - .id(); - - commands - .spawn(ButtonBundle { - style: Style { - // Size and position of text box - width: Val::Px(300.), - height: Val::Px(50.), - left: Val::Px(100.), - top: Val::Px(100.), - ..default() - }, - // needs to be set to prevent a bug where nothing is displayed - background_color: BackgroundColor(Color::WHITE), - ..default() - }) - .insert(CosmicSource(ui_editor)); - - // Sprite editor - commands.spawn((CosmicEditBundle { - sprite_bundle: SpriteBundle { - // Sets size of text box - sprite: Sprite { - custom_size: Some(Vec2::new(300., 100.)), - ..default() - }, - // Position of text box - transform: Transform::from_xyz(0., 100., 0.), - ..default() - }, - ..default() - },)); - - commands.insert_resource(Focus(Some(ui_editor))); -} - -fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) -} - -fn change_active_editor_ui( - mut commands: Commands, - mut interaction_query: Query< - (&Interaction, &CosmicSource), - (Changed<Interaction>, Without<ReadOnly>), - >, -) { - for (interaction, source) in interaction_query.iter_mut() { - if let Interaction::Pressed = interaction { - commands.insert_resource(Focus(Some(source.0))); - } - } -} - -fn change_active_editor_sprite( - mut commands: Commands, - windows: Query<&Window, With<PrimaryWindow>>, - buttons: Res<ButtonInput<MouseButton>>, - mut cosmic_edit_query: Query< - (&mut Sprite, &GlobalTransform, &Visibility, Entity), - (With<CosmicEditor>, Without<ReadOnly>), - >, - camera_q: Query<(&Camera, &GlobalTransform)>, -) { - let window = windows.single(); - let (camera, camera_transform) = camera_q.single(); - if buttons.just_pressed(MouseButton::Left) { - for (sprite, node_transform, visibility, entity) in &mut cosmic_edit_query.iter_mut() { - if visibility == Visibility::Hidden { - continue; - } - let size = sprite.custom_size.unwrap_or(Vec2::ONE); - let x_min = node_transform.affine().translation.x - size.x / 2.; - let y_min = node_transform.affine().translation.y - size.y / 2.; - let x_max = node_transform.affine().translation.x + size.x / 2.; - let y_max = node_transform.affine().translation.y + size.y / 2.; - if let Some(pos) = window.cursor_position() { - if let Some(pos) = camera.viewport_to_world_2d(camera_transform, pos) { - if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max { - commands.insert_resource(Focus(Some(entity))) - }; - } - }; - } - } -} - -fn ev_test( - mut evr_on: EventReader<TextHoverIn>, - mut evr_out: EventReader<TextHoverOut>, - mut evr_type: EventReader<CosmicTextChanged>, -) { - for _ev in evr_on.read() { - println!("IN"); - } - for _ev in evr_out.read() { - println!("OUT"); - } - for _ev in evr_type.read() { - println!("TYPE"); - } -} - -fn main() { - App::new() - .add_plugins(DefaultPlugins) - .add_plugins(CosmicEditPlugin { - change_cursor: CursorConfig::Default, - ..default() - }) - .add_systems(Startup, setup) - .add_systems(Update, change_active_editor_ui) - .add_systems(Update, change_active_editor_sprite) - .add_systems(Update, ev_test) - .run(); -} diff --git a/examples/every_option.rs b/examples/every_option.rs index 015dcc0b044bdb0dc3b9cb62921613cd5ec58c63..b6e3ba90beb6c7e9ed13ca60c355aff9dde211ae 100644 --- a/examples/every_option.rs +++ b/examples/every_option.rs @@ -1,30 +1,31 @@ -use bevy::{prelude::*, window::PrimaryWindow}; +use bevy::prelude::*; use bevy_cosmic_edit::*; +use util::{bevy_color_to_cosmic, change_active_editor_ui, deselect_editor_on_esc}; #[derive(Resource)] struct TextChangeTimer(pub Timer); -fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { +fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) { commands.spawn(Camera2dBundle::default()); - let attrs = - AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::rgb(0.27, 0.27, 0.27)))); - let primary_window = windows.single(); + let attrs = Attrs::new().color(bevy_color_to_cosmic(Color::rgb(0.27, 0.27, 0.27))); let editor = commands .spawn(CosmicEditBundle { + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(16., 16.)).with_text( + &mut font_system, + "Begin counting.", + attrs, + ), + cursor_color: CursorColor(Color::GREEN), + selection_color: SelectionColor(Color::PINK), + fill_color: FillColor(Color::YELLOW_GREEN), + x_offset: XOffset::default(), text_position: CosmicTextPosition::default(), - fill_color: FillColor::default(), background_image: CosmicBackground::default(), - attrs: CosmicAttrs(attrs.clone()), - metrics: CosmicMetrics { - font_size: 16., - line_height: 16., - scale_factor: primary_window.scale_factor() as f32, - }, + default_attrs: DefaultAttrs(AttrsOwned::new(attrs)), max_chars: CosmicMaxChars(15), max_lines: CosmicMaxLines(1), - text_setter: CosmicText::OneStyle("BANANA IS THE CODEWORD!".into()), mode: CosmicMode::Wrap, // CosmicEdit draws to this spritebundle sprite_bundle: SpriteBundle { @@ -43,12 +44,6 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { padding: Default::default(), widget_size: Default::default(), }) - .insert(CosmicEditPlaceholderBundle { - text_setter: PlaceholderText(CosmicText::OneStyle("Placeholder".into())), - attrs: PlaceholderAttrs(AttrsOwned::new( - Attrs::new().color(CosmicColor::rgb(88, 88, 88)), - )), - }) .id(); commands @@ -68,30 +63,19 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { }) .insert(CosmicSource(editor)); - commands.insert_resource(Focus(Some(editor))); - commands.insert_resource(TextChangeTimer(Timer::from_seconds( 1., TimerMode::Repeating, ))); } -pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) -} - // Test for update_buffer_text fn text_swapper( mut timer: ResMut<TextChangeTimer>, time: Res<Time>, - mut cosmic_q: Query<&mut CosmicText>, + mut cosmic_q: Query<(&mut CosmicBuffer, &DefaultAttrs)>, mut count: Local<usize>, - editor_q: Query<&CosmicEditor>, + mut font_system: ResMut<CosmicFontSystem>, ) { timer.0.tick(time.delta()); if !timer.0.just_finished() { @@ -99,12 +83,13 @@ fn text_swapper( } *count += 1; - for mut text in cosmic_q.iter_mut() { - text.set_if_neq(CosmicText::OneStyle(format!("TIMER {}", *count))); + for (mut buffer, attrs) in cosmic_q.iter_mut() { + buffer.set_text( + &mut font_system, + format!("Counting... {}", *count).as_str(), + attrs.as_attrs(), + ); } - - let editor = editor_q.single(); - println!("X OFFSET: {}", get_x_offset_center(50., editor.0.buffer())); } fn main() { @@ -113,5 +98,6 @@ fn main() { .add_plugins(CosmicEditPlugin::default()) .add_systems(Startup, setup) .add_systems(Update, text_swapper) + .add_systems(Update, (change_active_editor_ui, deselect_editor_on_esc)) .run(); } diff --git a/examples/font_per_widget.rs b/examples/font_per_widget.rs index f71241dcef6633374909e3de005c060774efac2d..f6f6959d825d7800134a9b529e9f8b8ba8ce5bc9 100644 --- a/examples/font_per_widget.rs +++ b/examples/font_per_widget.rs @@ -1,9 +1,10 @@ #![allow(clippy::type_complexity)] -use bevy::{prelude::*, window::PrimaryWindow}; +use bevy::prelude::*; use bevy_cosmic_edit::*; +use util::{bevy_color_to_cosmic, change_active_editor_ui, deselect_editor_on_esc}; -fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { +fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) { commands.spawn(Camera2dBundle::default()); let root = commands .spawn(NodeBundle { @@ -16,207 +17,91 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { ..default() }) .id(); - let primary_window = windows.single(); let attrs = Attrs::new(); let serif_attrs = attrs.family(Family::Serif); let mono_attrs = attrs.family(Family::Monospace); let comic_attrs = attrs.family(Family::Name("Comic Neue")); - let lines: Vec<Vec<(String, AttrsOwned)>> = vec![ - vec![ - ( - String::from("B"), - AttrsOwned::new(attrs.weight(FontWeight::BOLD)), - ), - (String::from("old "), AttrsOwned::new(attrs)), - ( - String::from("I"), - AttrsOwned::new(attrs.style(FontStyle::Italic)), - ), - (String::from("talic "), AttrsOwned::new(attrs)), - (String::from("f"), AttrsOwned::new(attrs)), - (String::from("i "), AttrsOwned::new(attrs)), - ( - String::from("f"), - AttrsOwned::new(attrs.weight(FontWeight::BOLD)), - ), - (String::from("i "), AttrsOwned::new(attrs)), - ( - String::from("f"), - AttrsOwned::new(attrs.style(FontStyle::Italic)), - ), - (String::from("i "), AttrsOwned::new(attrs)), - ], - vec![ - (String::from("Sans-Serif Normal "), AttrsOwned::new(attrs)), - ( - String::from("Sans-Serif Bold "), - AttrsOwned::new(attrs.weight(FontWeight::BOLD)), - ), - ( - String::from("Sans-Serif Italic "), - AttrsOwned::new(attrs.style(FontStyle::Italic)), - ), - ( - String::from("Sans-Serif Bold Italic"), - AttrsOwned::new(attrs.weight(FontWeight::BOLD).style(FontStyle::Italic)), - ), - ], - vec![ - (String::from("Serif Normal "), AttrsOwned::new(serif_attrs)), - ( - String::from("Serif Bold "), - AttrsOwned::new(serif_attrs.weight(FontWeight::BOLD)), - ), - ( - String::from("Serif Italic "), - AttrsOwned::new(serif_attrs.style(FontStyle::Italic)), - ), - ( - String::from("Serif Bold Italic"), - AttrsOwned::new( - serif_attrs - .weight(FontWeight::BOLD) - .style(FontStyle::Italic), - ), - ), - ], - vec![ - (String::from("Mono Normal "), AttrsOwned::new(mono_attrs)), - ( - String::from("Mono Bold "), - AttrsOwned::new(mono_attrs.weight(FontWeight::BOLD)), - ), - ( - String::from("Mono Italic "), - AttrsOwned::new(mono_attrs.style(FontStyle::Italic)), - ), - ( - String::from("Mono Bold Italic"), - AttrsOwned::new(mono_attrs.weight(FontWeight::BOLD).style(FontStyle::Italic)), - ), - ], - vec![ - (String::from("Comic Normal "), AttrsOwned::new(comic_attrs)), - ( - String::from("Comic Bold "), - AttrsOwned::new(comic_attrs.weight(FontWeight::BOLD)), - ), - ( - String::from("Comic Italic "), - AttrsOwned::new(comic_attrs.style(FontStyle::Italic)), - ), - ( - String::from("Comic Bold Italic"), - AttrsOwned::new( - comic_attrs - .weight(FontWeight::BOLD) - .style(FontStyle::Italic), - ), - ), - ], - vec![ - ( - String::from("R"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))), - ), - ( - String::from("A"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::ORANGE))), - ), - ( - String::from("I"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::YELLOW))), - ), - ( - String::from("N"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::GREEN))), - ), - ( - String::from("B"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::BLUE))), - ), - ( - String::from("O"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::INDIGO))), - ), - ( - String::from("W "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::PURPLE))), - ), - ( - String::from("Red "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))), - ), - ( - String::from("Orange "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::ORANGE))), - ), - ( - String::from("Yellow "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::YELLOW))), - ), - ( - String::from("Green "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::GREEN))), - ), - ( - String::from("Blue "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::BLUE))), - ), - ( - String::from("Indigo "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::INDIGO))), - ), - ( - String::from("Violet "), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::PURPLE))), - ), - ( - String::from("U"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::PURPLE))), - ), - ( - String::from("N"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::INDIGO))), - ), - ( - String::from("I"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::BLUE))), - ), - ( - String::from("C"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::GREEN))), - ), - ( - String::from("O"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::YELLOW))), - ), - ( - String::from("R"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::ORANGE))), - ), - ( - String::from("N"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))), - ), - ], - vec![( - String::from("鐢熸椿,靷�,啶溹た啶傕う啶椸 馃榾 FPS"), - AttrsOwned::new(attrs.color(bevy_color_to_cosmic(Color::RED))), - )], + let lines = vec![ + ("B", attrs.weight(FontWeight::BOLD)), + ("old ", attrs), + ("I", attrs.style(FontStyle::Italic)), + ("talic ", attrs), + ("f", attrs), + ("i ", attrs), + ("f", attrs.weight(FontWeight::BOLD)), + ("i ", attrs), + ("f", attrs.style(FontStyle::Italic)), + ("i ", attrs), + ("Sans-Serif Normal ", attrs), + ("Sans-Serif Bold ", attrs.weight(FontWeight::BOLD)), + ("Sans-Serif Italic ", attrs.style(FontStyle::Italic)), + ( + "Sans-Serif Bold Italic", + attrs.weight(FontWeight::BOLD).style(FontStyle::Italic), + ), + ("Serif Normal ", serif_attrs), + ("Serif Bold ", serif_attrs.weight(FontWeight::BOLD)), + ("Serif Italic ", serif_attrs.style(FontStyle::Italic)), + ( + "Serif Bold Italic", + serif_attrs + .weight(FontWeight::BOLD) + .style(FontStyle::Italic), + ), + ("\n", attrs), + ("Mono Normal ", mono_attrs), + ("Mono Bold ", mono_attrs.weight(FontWeight::BOLD)), + ("Mono Italic ", mono_attrs.style(FontStyle::Italic)), + ( + "Mono Bold Italic", + mono_attrs.weight(FontWeight::BOLD).style(FontStyle::Italic), + ), + ("Comic Normal ", comic_attrs), + ("Comic Bold ", comic_attrs.weight(FontWeight::BOLD)), + ("Comic Italic ", comic_attrs.style(FontStyle::Italic)), + ( + "Comic Bold Italic", + comic_attrs + .weight(FontWeight::BOLD) + .style(FontStyle::Italic), + ), + ("\n", attrs), + ("R", attrs.color(bevy_color_to_cosmic(Color::RED))), + ("A", attrs.color(bevy_color_to_cosmic(Color::ORANGE))), + ("I", attrs.color(bevy_color_to_cosmic(Color::YELLOW))), + ("N", attrs.color(bevy_color_to_cosmic(Color::GREEN))), + ("B", attrs.color(bevy_color_to_cosmic(Color::BLUE))), + ("O", attrs.color(bevy_color_to_cosmic(Color::INDIGO))), + ("W ", attrs.color(bevy_color_to_cosmic(Color::PURPLE))), + ("Red ", attrs.color(bevy_color_to_cosmic(Color::RED))), + ("Orange ", attrs.color(bevy_color_to_cosmic(Color::ORANGE))), + ("Yellow ", attrs.color(bevy_color_to_cosmic(Color::YELLOW))), + ("Green ", attrs.color(bevy_color_to_cosmic(Color::GREEN))), + ("Blue ", attrs.color(bevy_color_to_cosmic(Color::BLUE))), + ("Indigo ", attrs.color(bevy_color_to_cosmic(Color::INDIGO))), + ("Violet ", attrs.color(bevy_color_to_cosmic(Color::PURPLE))), + ("U", attrs.color(bevy_color_to_cosmic(Color::PURPLE))), + ("N", attrs.color(bevy_color_to_cosmic(Color::INDIGO))), + ("I", attrs.color(bevy_color_to_cosmic(Color::BLUE))), + ("C", attrs.color(bevy_color_to_cosmic(Color::GREEN))), + ("O", attrs.color(bevy_color_to_cosmic(Color::YELLOW))), + ("R", attrs.color(bevy_color_to_cosmic(Color::ORANGE))), + ("N", attrs.color(bevy_color_to_cosmic(Color::RED))), + ( + "鐢熸椿,靷�,啶溹た啶傕う啶椸 馃榾 FPS", + attrs.color(bevy_color_to_cosmic(Color::RED)), + ), ]; let cosmic_edit_1 = commands .spawn(CosmicEditBundle { + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(18., 22.)).with_rich_text( + &mut font_system, + lines, + attrs, + ), text_position: bevy_cosmic_edit::CosmicTextPosition::Center, - attrs: CosmicAttrs(AttrsOwned::new(attrs)), - metrics: CosmicMetrics { - font_size: 18., - line_height: 22., - scale_factor: primary_window.scale_factor() as f32, - }, - text_setter: CosmicText::MultiStyle(lines), ..default() }) .id(); @@ -227,14 +112,12 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { let cosmic_edit_2 = commands .spawn(CosmicEditBundle { - attrs: CosmicAttrs(AttrsOwned::new(attrs_2)), - metrics: CosmicMetrics { - font_size: 28., - line_height: 36., - scale_factor: primary_window.scale_factor() as f32, - }, + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(28., 36.)).with_text( + &mut font_system, + "Widget 2.\nClick on me =>", + attrs_2, + ), text_position: CosmicTextPosition::Center, - text_setter: CosmicText::OneStyle("Widget 2.\nClick on me =>".to_string()), ..default() }) .id(); @@ -265,48 +148,13 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { }) .insert(CosmicSource(cosmic_edit_2)); }); - - // Set active editor - commands.insert_resource(Focus(Some(cosmic_edit_1))); -} - -fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) -} - -fn change_active_editor_ui( - mut commands: Commands, - mut interaction_query: Query< - (&Interaction, &CosmicSource), - (Changed<Interaction>, Without<ReadOnly>), - >, -) { - for (interaction, source) in interaction_query.iter_mut() { - if let Interaction::Pressed = interaction { - commands.insert_resource(Focus(Some(source.0))); - } - } } fn main() { - let font_config = CosmicFontConfig { - fonts_dir_path: None, - font_bytes: None, - load_system_fonts: true, - }; - App::new() .add_plugins(DefaultPlugins) - .add_plugins(CosmicEditPlugin { - font_config, - ..default() - }) + .add_plugins(CosmicEditPlugin { ..default() }) .add_systems(Startup, setup) - .add_systems(Update, change_active_editor_ui) + .add_systems(Update, (change_active_editor_ui, deselect_editor_on_esc)) .run(); } diff --git a/examples/image_background.rs b/examples/image_background.rs index 830126b85c38b6b396f37ada428b9a110dc0f7ac..3d9b404260569b5f6c9194f76e8fc3d786a2b30f 100644 --- a/examples/image_background.rs +++ b/examples/image_background.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; use bevy_cosmic_edit::*; +use util::{bevy_color_to_cosmic, change_active_editor_ui, deselect_editor_on_esc}; fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn(Camera2dBundle::default()); @@ -8,7 +9,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { let editor = commands .spawn(CosmicEditBundle { - attrs: CosmicAttrs(AttrsOwned::new( + default_attrs: DefaultAttrs(AttrsOwned::new( Attrs::new().color(bevy_color_to_cosmic(Color::GREEN)), )), background_image: CosmicBackground(Some(bg_image_handle)), @@ -30,17 +31,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { ..default() }) .insert(CosmicSource(editor)); - - commands.insert_resource(Focus(Some(editor))); -} - -pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) } fn main() { @@ -48,5 +38,6 @@ fn main() { .add_plugins(DefaultPlugins) .add_plugins(CosmicEditPlugin::default()) .add_systems(Startup, setup) + .add_systems(Update, (change_active_editor_ui, deselect_editor_on_esc)) .run(); } diff --git a/examples/login.rs b/examples/login.rs deleted file mode 100644 index 79b86b0ccd48da030af2ad507066a82a7d71501f..0000000000000000000000000000000000000000 --- a/examples/login.rs +++ /dev/null @@ -1,269 +0,0 @@ -use bevy::{ - prelude::*, - window::{PrimaryWindow, WindowResolution}, -}; -use bevy_cosmic_edit::*; - -#[derive(Component)] -struct SubmitButton; - -#[derive(Component)] -struct UsernameTag; - -#[derive(Component)] -struct PasswordTag; - -#[derive(Component)] -struct DisplayTag; - -fn setup(mut commands: Commands, window: Query<&Window, With<PrimaryWindow>>) { - let window = window.single(); - - commands.spawn(Camera2dBundle::default()); - - let login_editor = commands - .spawn(CosmicEditBundle { - max_lines: CosmicMaxLines(1), - metrics: CosmicMetrics { - scale_factor: window.scale_factor() as f32, - ..default() - }, - sprite_bundle: SpriteBundle { - sprite: Sprite { - custom_size: Some(Vec2::new(300.0, 50.0)), - ..default() - }, - visibility: Visibility::Hidden, - ..default() - }, - ..default() - }) - .insert(CosmicEditPlaceholderBundle { - text_setter: PlaceholderText(CosmicText::OneStyle("Username".into())), - attrs: PlaceholderAttrs(AttrsOwned::new( - Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))), - )), - }) - .insert(UsernameTag) - .id(); - - let password_editor = commands - .spawn(CosmicEditBundle { - max_lines: CosmicMaxLines(1), - metrics: CosmicMetrics { - scale_factor: window.scale_factor() as f32, - ..default() - }, - ..default() - }) - .insert(CosmicEditPlaceholderBundle { - text_setter: PlaceholderText(CosmicText::OneStyle("Password".into())), - attrs: PlaceholderAttrs(AttrsOwned::new( - Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))), - )), - }) - .insert(PasswordTag) - .insert(PasswordInput::default()) - .id(); - - let submit_editor = commands - .spawn(CosmicEditBundle { - max_lines: CosmicMaxLines(1), - metrics: CosmicMetrics { - font_size: 25.0, - line_height: 25.0, - scale_factor: window.scale_factor() as f32, - ..default() - }, - attrs: CosmicAttrs(AttrsOwned::new( - Attrs::new().color(bevy_color_to_cosmic(Color::WHITE)), - )), - text_setter: CosmicText::OneStyle("Submit".into()), - fill_color: FillColor(Color::GREEN), - ..default() - }) - .insert(ReadOnly) - .id(); - - let display_editor = commands - .spawn(CosmicEditBundle { - metrics: CosmicMetrics { - scale_factor: window.scale_factor() as f32, - ..default() - }, - ..default() - }) - .insert(CosmicEditPlaceholderBundle { - text_setter: PlaceholderText(CosmicText::OneStyle("Output".into())), - attrs: PlaceholderAttrs(AttrsOwned::new( - Attrs::new().color(bevy_color_to_cosmic(Color::rgb_u8(128, 128, 128))), - )), - }) - .insert((ReadOnly, DisplayTag)) - .id(); - - commands.insert_resource(Focus(Some(login_editor))); - - // Spawn UI - commands - .spawn(NodeBundle { - style: Style { - flex_direction: FlexDirection::Column, - align_items: AlignItems::Center, - padding: UiRect::all(Val::Px(15.0)), - width: Val::Px(330.0), - - ..default() - }, - ..default() - }) - .with_children(|root| { - root.spawn(ButtonBundle { - style: Style { - // Size and position of text box - width: Val::Px(300.), - height: Val::Px(50.), - margin: UiRect::all(Val::Px(15.0)), - ..default() - }, - background_color: BackgroundColor(Color::WHITE), - ..default() - }) - .insert(CosmicSource(login_editor)); - - root.spawn(ButtonBundle { - style: Style { - // Size and position of text box - width: Val::Px(300.), - height: Val::Px(50.), - margin: UiRect::all(Val::Px(15.0)), - ..default() - }, - background_color: BackgroundColor(Color::WHITE), - ..default() - }) - .insert(CosmicSource(password_editor)); - - root.spawn(ButtonBundle { - style: Style { - // Size and position of text box - width: Val::Px(150.), - height: Val::Px(50.), - margin: UiRect::all(Val::Px(15.0)), - border: UiRect::all(Val::Px(3.0)), - ..default() - }, - background_color: BackgroundColor(Color::WHITE), - border_color: Color::DARK_GREEN.into(), - - ..default() - }) - .insert(SubmitButton) - .insert(CosmicSource(submit_editor)); - - root.spawn(ButtonBundle { - style: Style { - // Size and position of text box - width: Val::Px(300.), - height: Val::Px(100.), - margin: UiRect::all(Val::Px(15.0)), - ..default() - }, - background_color: BackgroundColor(Color::WHITE), - ..default() - }) - .insert(CosmicSource(display_editor)); - }); -} - -fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) -} - -fn change_active_editor_ui( - mut commands: Commands, - mut interaction_query: Query< - (&Interaction, &CosmicSource), - (Changed<Interaction>, Without<ReadOnly>), - >, -) { - for (interaction, source) in interaction_query.iter_mut() { - if let Interaction::Pressed = interaction { - commands.insert_resource(Focus(Some(source.0))); - } - } -} - -fn print_changed_input(mut evr_type: EventReader<CosmicTextChanged>) { - for ev in evr_type.read() { - println!("Changed: {}", ev.0 .1); - } -} - -fn submit_button( - button_q: Query<&Interaction, With<SubmitButton>>, - username_q: Query< - &CosmicEditor, - (With<UsernameTag>, Without<PasswordTag>, Without<DisplayTag>), - >, - password_q: Query< - &CosmicEditor, - (With<PasswordTag>, Without<UsernameTag>, Without<DisplayTag>), - >, - mut display_q: Query< - (&mut CosmicEditor, &CosmicAttrs), - (With<DisplayTag>, Without<UsernameTag>, Without<PasswordTag>), - >, - mut font_system: ResMut<CosmicFontSystem>, - mut window: Query<&mut Window, With<PrimaryWindow>>, -) { - for interaction in button_q.iter() { - match interaction { - Interaction::None => {} - Interaction::Pressed => { - let u = username_q.single(); - let p = password_q.single(); - let (mut d, attrs) = display_q.single_mut(); - - let text = format!( - "Submitted!\nUsername: {}\nPassword: {}\n", - u.get_text(), - p.get_text() - ); - - d.set_text( - CosmicText::OneStyle(text), - attrs.0.clone(), - &mut font_system.0, - ); - } - Interaction::Hovered => { - window.single_mut().cursor.icon = CursorIcon::Pointer; - } - } - } -} - -fn main() { - App::new() - .add_plugins(DefaultPlugins.set(WindowPlugin { - primary_window: Some(Window { - resolution: WindowResolution::new(330., 480.), - ..default() - }), - ..default() - })) - .add_plugins(CosmicEditPlugin { - change_cursor: CursorConfig::Default, - ..default() - }) - .add_systems(Startup, setup) - .add_systems(Update, change_active_editor_ui) - .add_systems(Update, (print_changed_input, submit_button)) - .run(); -} diff --git a/examples/multiple_sprites.rs b/examples/multiple_sprites.rs index 04f5f867c589fbd916802d110b6b3c344bb2be3f..13d15f419143813b983dc5ec700b9e95ca2dc179 100644 --- a/examples/multiple_sprites.rs +++ b/examples/multiple_sprites.rs @@ -1,7 +1,12 @@ use bevy::{prelude::*, window::PrimaryWindow}; use bevy_cosmic_edit::*; +use util::{bevy_color_to_cosmic, change_active_editor_sprite}; -fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { +fn setup( + mut commands: Commands, + windows: Query<&Window, With<PrimaryWindow>>, + mut font_system: ResMut<CosmicFontSystem>, +) { let primary_window = windows.single(); let camera_bundle = Camera2dBundle { camera: Camera { @@ -15,18 +20,15 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { let mut attrs = Attrs::new(); attrs = attrs.family(Family::Name("Victor Mono")); attrs = attrs.color(bevy_color_to_cosmic(Color::PURPLE)); - let metrics = CosmicMetrics { - font_size: 14., - line_height: 18., - scale_factor: primary_window.scale_factor() as f32, - }; - let cosmic_edit_1 = (CosmicEditBundle { - attrs: CosmicAttrs(AttrsOwned::new(attrs)), - metrics: metrics.clone(), + commands.spawn(CosmicEditBundle { text_position: CosmicTextPosition::Center, fill_color: FillColor(Color::ALICE_BLUE), - text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y".to_string()), + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text( + &mut font_system, + "馃榾馃榾馃榾 x => y", + attrs, + ), sprite_bundle: SpriteBundle { sprite: Sprite { custom_size: Some(Vec2 { @@ -39,14 +41,16 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { ..default() }, ..default() - },); + }); - let cosmic_edit_2 = (CosmicEditBundle { - attrs: CosmicAttrs(AttrsOwned::new(attrs)), - metrics, + commands.spawn(CosmicEditBundle { text_position: CosmicTextPosition::Center, fill_color: FillColor(Color::GRAY.with_a(0.5)), - text_setter: CosmicText::OneStyle("Widget_2. Click on me".to_string()), + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text( + &mut font_system, + "Widget_2. Click on me", + attrs, + ), sprite_bundle: SpriteBundle { sprite: Sprite { custom_size: Some(Vec2 { @@ -63,52 +67,7 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { ..default() }, ..default() - },); - - let id = commands.spawn(cosmic_edit_1).id(); - - commands.insert_resource(Focus(Some(id))); - - commands.spawn(cosmic_edit_2); -} - -fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) -} - -fn change_active_editor_sprite( - mut commands: Commands, - windows: Query<&Window, With<PrimaryWindow>>, - buttons: Res<ButtonInput<MouseButton>>, - mut cosmic_edit_query: Query< - (&mut Sprite, &GlobalTransform, Entity), - (With<CosmicEditor>, Without<ReadOnly>), - >, - camera_q: Query<(&Camera, &GlobalTransform)>, -) { - let window = windows.single(); - let (camera, camera_transform) = camera_q.single(); - if buttons.just_pressed(MouseButton::Left) { - for (sprite, node_transform, entity) in &mut cosmic_edit_query.iter_mut() { - let size = sprite.custom_size.unwrap_or(Vec2::new(1., 1.)); - let x_min = node_transform.affine().translation.x - size.x / 2.; - let y_min = node_transform.affine().translation.y - size.y / 2.; - let x_max = node_transform.affine().translation.x + size.x / 2.; - let y_max = node_transform.affine().translation.y + size.y / 2.; - if let Some(pos) = window.cursor_position() { - if let Some(pos) = camera.viewport_to_world_2d(camera_transform, pos) { - if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max { - commands.insert_resource(Focus(Some(entity))) - }; - } - }; - } - } + }); } fn main() { diff --git a/examples/readonly.rs b/examples/readonly.rs index 0f50de8a37d76f1d31cdeecc688f7c7b538ec86e..b41ad9f393a78a68bc971b1911bca9f1c7a1eb52 100644 --- a/examples/readonly.rs +++ b/examples/readonly.rs @@ -1,8 +1,8 @@ -use bevy::{prelude::*, window::PrimaryWindow}; +use bevy::prelude::*; use bevy_cosmic_edit::*; +use util::{bevy_color_to_cosmic, change_active_editor_ui}; -fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { - let primary_window = windows.single(); +fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) { commands.spawn(Camera2dBundle::default()); let root = commands .spawn(NodeBundle { @@ -23,14 +23,12 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { // spawn editor let cosmic_edit = commands .spawn(CosmicEditBundle { - attrs: CosmicAttrs(AttrsOwned::new(attrs)), text_position: CosmicTextPosition::Center, - metrics: CosmicMetrics { - font_size: 14., - line_height: 18., - scale_factor: primary_window.scale_factor() as f32, - }, - text_setter: CosmicText::OneStyle("馃榾馃榾馃榾 x => y\nRead only widget".to_string()), + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(14., 18.)).with_text( + &mut font_system, + "馃榾馃榾馃榾 x => y\nRead only widget", + attrs, + ), ..default() }) .insert(ReadOnly) @@ -51,17 +49,6 @@ fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { // add cosmic source .insert(CosmicSource(cosmic_edit)); }); - - commands.insert_resource(Focus(Some(cosmic_edit))); -} - -pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) } fn main() { @@ -79,5 +66,6 @@ fn main() { ..default() }) .add_systems(Startup, setup) + .add_systems(Update, change_active_editor_ui) .run(); } diff --git a/examples/sprite_and_ui_clickable.rs b/examples/sprite_and_ui_clickable.rs new file mode 100644 index 0000000000000000000000000000000000000000..31d4e76ce8f5f033d2ca9879082fc40ab209681a --- /dev/null +++ b/examples/sprite_and_ui_clickable.rs @@ -0,0 +1,86 @@ +use bevy::prelude::*; +use bevy_cosmic_edit::*; +use util::{ + bevy_color_to_cosmic, change_active_editor_sprite, change_active_editor_ui, + deselect_editor_on_esc, +}; + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); + + // UI editor + let ui_editor = commands + .spawn(CosmicEditBundle { + default_attrs: DefaultAttrs(AttrsOwned::new( + Attrs::new().color(bevy_color_to_cosmic(Color::GREEN)), + )), + max_lines: CosmicMaxLines(1), + ..default() + }) + .id(); + + commands + .spawn(ButtonBundle { + style: Style { + // Size and position of text box + width: Val::Px(300.), + height: Val::Px(50.), + left: Val::Px(100.), + top: Val::Px(100.), + ..default() + }, + ..default() + }) + .insert(CosmicSource(ui_editor)); + + // Sprite editor + commands.spawn((CosmicEditBundle { + sprite_bundle: SpriteBundle { + // Sets size of text box + sprite: Sprite { + custom_size: Some(Vec2::new(300., 100.)), + ..default() + }, + // Position of text box + transform: Transform::from_xyz(0., 100., 0.), + ..default() + }, + ..default() + },)); +} + +fn ev_test( + mut evr_on: EventReader<TextHoverIn>, + mut evr_out: EventReader<TextHoverOut>, + mut evr_type: EventReader<CosmicTextChanged>, +) { + for _ev in evr_on.read() { + println!("IN"); + } + for _ev in evr_out.read() { + println!("OUT"); + } + for _ev in evr_type.read() { + println!("TYPE"); + } +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CosmicEditPlugin { + change_cursor: CursorConfig::Default, + ..default() + }) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + change_active_editor_ui, + change_active_editor_sprite, + deselect_editor_on_esc, + ), + ) + .add_systems(Update, ev_test) + .run(); +} diff --git a/examples/text_input.rs b/examples/text_input.rs deleted file mode 100644 index 89dce6d96c9d8bab8113d52eabc8c7d933d7ecfe..0000000000000000000000000000000000000000 --- a/examples/text_input.rs +++ /dev/null @@ -1,162 +0,0 @@ -use bevy::{ - prelude::*, - window::{PresentMode, PrimaryWindow}, -}; -use bevy_cosmic_edit::*; - -fn create_editable_widget(commands: &mut Commands, scale_factor: f32, text: String) -> Entity { - let attrs = - AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::hex("4d4d4d").unwrap()))); - let placeholder_attrs = - AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::hex("#e6e6e6").unwrap()))); - let editor = commands - .spawn(( - CosmicEditBundle { - attrs: CosmicAttrs(attrs.clone()), - metrics: CosmicMetrics { - font_size: 18., - line_height: 18. * 1.2, - scale_factor, - }, - max_lines: CosmicMaxLines(1), - text_setter: CosmicText::OneStyle(text), - text_position: CosmicTextPosition::Left { padding: 20 }, - mode: CosmicMode::InfiniteLine, - ..default() - }, - CosmicEditPlaceholderBundle { - text_setter: PlaceholderText(CosmicText::OneStyle("Type something...".into())), - attrs: PlaceholderAttrs(placeholder_attrs.clone()), - }, - )) - .id(); - commands - .spawn(ButtonBundle { - border_color: Color::hex("#ededed").unwrap().into(), - style: Style { - border: UiRect::all(Val::Px(3.)), - width: Val::Percent(20.), - height: Val::Px(50.), - left: Val::Percent(40.), - top: Val::Px(100.), - ..default() - }, - background_color: Color::WHITE.into(), - ..default() - }) - .insert(CosmicSource(editor)); - - editor -} - -fn create_readonly_widget(commands: &mut Commands, scale_factor: f32, text: String) -> Entity { - let attrs = - AttrsOwned::new(Attrs::new().color(bevy_color_to_cosmic(Color::hex("4d4d4d").unwrap()))); - - let editor = commands - .spawn(( - CosmicEditBundle { - attrs: CosmicAttrs(attrs.clone()), - metrics: CosmicMetrics { - font_size: 18., - line_height: 18. * 1.2, - scale_factor, - }, - text_setter: CosmicText::OneStyle(text), - text_position: CosmicTextPosition::Left { padding: 20 }, - mode: CosmicMode::AutoHeight, - ..default() - }, - ReadOnly, - )) - .id(); - - commands - .spawn(ButtonBundle { - border_color: Color::hex("#ededed").unwrap().into(), - style: Style { - border: UiRect::all(Val::Px(3.)), - width: Val::Percent(20.), - height: Val::Px(50.), - left: Val::Percent(40.), - top: Val::Px(100.), - ..default() - }, - background_color: Color::WHITE.into(), - ..default() - }) - .insert(CosmicSource(editor)); - - editor -} - -fn setup(mut commands: Commands, windows: Query<&Window, With<PrimaryWindow>>) { - commands.spawn(Camera2dBundle::default()); - let primary_window = windows.single(); - let editor = create_editable_widget( - &mut commands, - primary_window.scale_factor() as f32, - "".to_string(), - ); - commands.insert_resource(Focus(Some(editor))); -} - -fn handle_enter( - mut commands: Commands, - keys: Res<ButtonInput<KeyCode>>, - mut query_dest: Query<(Entity, &CosmicSource)>, - mut query_source: Query<(Entity, &CosmicEditor, &CosmicMode)>, - windows: Query<&Window, With<PrimaryWindow>>, -) { - if keys.just_pressed(KeyCode::Enter) { - let scale_factor = windows.single().scale_factor() as f32; - for (entity, editor, mode) in query_source.iter_mut() { - // Remove UI elements - for (dest_entity, source) in query_dest.iter_mut() { - if source.0 == entity { - commands.entity(dest_entity).despawn_recursive(); - } - } - - let text = editor.get_text(); - commands.entity(entity).despawn_recursive(); - if *mode == CosmicMode::AutoHeight { - let editor = create_editable_widget(&mut commands, scale_factor, text); - commands.insert_resource(Focus(Some(editor))); - } else { - let editor = create_readonly_widget(&mut commands, scale_factor, text); - commands.insert_resource(Focus(Some(editor))); - }; - } - } -} - -fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { - CosmicColor::rgba( - (color.r() * 255.) as u8, - (color.g() * 255.) as u8, - (color.b() * 255.) as u8, - (color.a() * 255.) as u8, - ) -} - -fn main() { - App::new() - .add_plugins( - DefaultPlugins - .set(WindowPlugin { - primary_window: Some(Window { - title: "bevy 鈥� text_input".into(), - present_mode: PresentMode::AutoVsync, - // TODO reimplement fit to parent - ..default() - }), - ..default() - }) - .build(), - ) - .add_plugins(CosmicEditPlugin::default()) - .add_systems(Update, handle_enter) - .add_systems(Startup, setup) - .run(); -} diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..8f58564057c5fbf66dda42fba81c57c10aedbc02 --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,220 @@ +use crate::*; +use bevy::{prelude::*, window::PrimaryWindow}; + +#[derive(Component, Deref, DerefMut)] +pub struct CosmicBuffer(pub Buffer); + +impl Default for CosmicBuffer { + fn default() -> Self { + CosmicBuffer(Buffer::new_empty(Metrics::new(20., 20.))) + } +} + +impl<'s, 'r> CosmicBuffer { + pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self { + Self(Buffer::new(font_system, metrics)) + } + + // TODO: Set redraw when setting text + + // Das a lotta boilerplate just to hide the shaping argument + pub fn with_text( + mut self, + font_system: &mut FontSystem, + text: &'s str, + attrs: Attrs<'r>, + ) -> Self { + self.0.set_text(font_system, text, attrs, Shaping::Advanced); + self + } + + pub fn with_rich_text<I>( + mut self, + font_system: &mut FontSystem, + spans: I, + attrs: Attrs<'r>, + ) -> Self + where + I: IntoIterator<Item = (&'s str, Attrs<'r>)>, + { + self.0 + .set_rich_text(font_system, spans, attrs, Shaping::Advanced); + self + } + + pub fn set_text( + &mut self, + font_system: &mut FontSystem, + text: &'s str, + attrs: Attrs<'r>, + ) -> &mut Self { + self.0.set_text(font_system, text, attrs, Shaping::Advanced); + self.set_redraw(true); + self + } + + pub fn set_rich_text<I>( + &mut self, + font_system: &mut FontSystem, + spans: I, + attrs: Attrs<'r>, + ) -> &mut Self + where + I: IntoIterator<Item = (&'s str, Attrs<'r>)>, + { + self.0 + .set_rich_text(font_system, spans, attrs, Shaping::Advanced); + self.set_redraw(true); + self + } + + /// Retrieves the cosmic text content from a buffer. + /// + /// # Arguments + /// + /// * none, takes the rust magic ref to self + /// + /// # Returns + /// + /// A `String` containing the cosmic text content. + pub fn get_text(&self) -> String { + let mut text = String::new(); + let line_count = self.lines.len(); + + for (i, line) in self.lines.iter().enumerate() { + text.push_str(line.text()); + + if i < line_count - 1 { + text.push('\n'); + } + } + + text + } + /// Returns texts from a MultiStyle buffer + pub fn get_text_spans(&self, default_attrs: AttrsOwned) -> Vec<Vec<(String, AttrsOwned)>> { + // TODO: untested! + + let buffer = self; + + let mut spans = Vec::new(); + for line in buffer.lines.iter() { + let mut line_spans = Vec::new(); + let line_text = line.text(); + let line_attrs = line.attrs_list(); + if line_attrs.spans().is_empty() { + line_spans.push((line_text.to_string(), default_attrs.clone())); + } else { + let mut current_pos = 0; + for span in line_attrs.spans() { + let span_range = span.0; + let span_attrs = span.1.clone(); + let start_index = span_range.start; + let end_index = span_range.end; + if start_index > current_pos { + // Add the text between the current position and the start of the span + let non_span_text = line_text[current_pos..start_index].to_string(); + line_spans.push((non_span_text, default_attrs.clone())); + } + let span_text = line_text[start_index..end_index].to_string(); + line_spans.push((span_text.clone(), span_attrs)); + current_pos = end_index; + } + if current_pos < line_text.len() { + // Add the remaining text after the last span + let remaining_text = line_text[current_pos..].to_string(); + line_spans.push((remaining_text, default_attrs.clone())); + } + } + spans.push(line_spans); + } + spans + } +} + +pub fn add_font_system( + mut font_system: ResMut<CosmicFontSystem>, + mut q: Query<&mut CosmicBuffer, Added<CosmicBuffer>>, +) { + for mut b in q.iter_mut() { + if !b.lines.is_empty() { + continue; + } + b.0.set_text(&mut font_system, "", Attrs::new(), Shaping::Advanced); + b.set_redraw(true); + } +} + +pub fn set_initial_scale( + window_q: Query<&Window, With<PrimaryWindow>>, + mut cosmic_query: Query<&mut CosmicBuffer, Added<CosmicBuffer>>, + mut font_system: ResMut<CosmicFontSystem>, +) { + let w_scale = window_q.single().scale_factor(); + + for mut b in &mut cosmic_query.iter_mut() { + let m = b.metrics().scale(w_scale); + b.set_metrics(&mut font_system, m); + } +} + +pub fn set_redraw(mut q: Query<&mut CosmicBuffer, Added<CosmicBuffer>>) { + for mut b in q.iter_mut() { + b.set_redraw(true); + } +} + +pub fn set_editor_redraw(mut q: Query<&mut CosmicEditor, Added<CosmicEditor>>) { + for mut b in q.iter_mut() { + b.set_redraw(true); + } +} + +pub(crate) fn swap_target_handle( + source_q: Query<&Handle<Image>, With<CosmicBuffer>>, + mut dest_q: Query< + ( + Option<&mut Handle<Image>>, + Option<&mut UiImage>, + &CosmicSource, + ), + Without<CosmicBuffer>, + >, +) { + // TODO: do this once + for (dest_handle_opt, dest_ui_opt, source_entity) in dest_q.iter_mut() { + if let Ok(source_handle) = source_q.get(source_entity.0) { + if let Some(mut dest_handle) = dest_handle_opt { + *dest_handle = source_handle.clone_weak(); + } + if let Some(mut dest_ui) = dest_ui_opt { + dest_ui.texture = source_handle.clone_weak(); + } + } + } +} + +// TODO put this on impl CosmicBuffer + +pub fn get_text_size(buffer: &Buffer) -> (f32, f32) { + if buffer.layout_runs().count() == 0 { + return (0., buffer.metrics().line_height); + } + let width = buffer + .layout_runs() + .map(|run| run.line_w) + .reduce(f32::max) + .unwrap(); + let height = buffer.layout_runs().count() as f32 * buffer.metrics().line_height; + (width, height) +} + +pub fn get_y_offset_center(widget_height: f32, buffer: &Buffer) -> i32 { + let (_, text_height) = get_text_size(buffer); + ((widget_height - text_height) / 2.0) as i32 +} + +pub fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 { + let (text_width, _) = get_text_size(buffer); + ((widget_width - text_width) / 2.0) as i32 +} diff --git a/src/cursor.rs b/src/cursor.rs index 52fbfa866f63ab7feb1121f4de08a75a0dde5232..a044bcfb7d63a78b2c415dbc684e19f0ab9f42ff 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,6 +1,6 @@ use bevy::{input::mouse::MouseMotion, prelude::*, window::PrimaryWindow}; -use crate::{CosmicEditor, CosmicSource, CosmicTextChanged}; +use crate::{CosmicBuffer, CosmicSource, CosmicTextChanged}; #[cfg(feature = "multicam")] use crate::CosmicPrimaryCamera; @@ -48,7 +48,7 @@ type CameraQuery<'a, 'b, 'c, 'd> = Query<'a, 'b, (&'c Camera, &'d GlobalTransfor pub fn hover_sprites( windows: Query<&Window, With<PrimaryWindow>>, - mut cosmic_edit_query: Query<(&mut Sprite, &Visibility, &GlobalTransform), With<CosmicEditor>>, + mut cosmic_edit_query: Query<(&mut Sprite, &Visibility, &GlobalTransform), With<CosmicBuffer>>, camera_q: CameraQuery, mut hovered: Local<bool>, mut last_hovered: Local<bool>, @@ -65,6 +65,7 @@ pub fn hover_sprites( if visibility == Visibility::Hidden { continue; } + let size = sprite.custom_size.unwrap_or(Vec2::ONE); let x_min = node_transform.affine().translation.x - size.x / 2.; let y_min = node_transform.affine().translation.y - size.y / 2.; diff --git a/src/focus.rs b/src/focus.rs new file mode 100644 index 0000000000000000000000000000000000000000..6520f0dd77491b2d01848d6c85b3869149067f99 --- /dev/null +++ b/src/focus.rs @@ -0,0 +1,45 @@ +use bevy::prelude::*; +use cosmic_text::{Edit, Editor}; + +use crate::{CosmicBuffer, CosmicEditor}; + +/// Resource struct that keeps track of the currently active editor entity. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct FocusedWidget(pub Option<Entity>); + +pub(crate) fn add_editor_to_focused( + mut commands: Commands, + active_editor: Res<FocusedWidget>, + q: Query<&CosmicBuffer, Without<CosmicEditor>>, +) { + if let Some(e) = active_editor.0 { + let Ok(b) = q.get(e) else { + return; + }; + let mut editor = Editor::new(b.0.clone()); + editor.set_redraw(true); + commands.entity(e).insert(CosmicEditor::new(editor)); + } +} + +pub(crate) fn drop_editor_unfocused( + mut commands: Commands, + active_editor: Res<FocusedWidget>, + mut q: Query<(Entity, &mut CosmicBuffer, &CosmicEditor)>, +) { + if active_editor.0.is_none() { + for (e, mut b, ed) in q.iter_mut() { + b.lines = ed.with_buffer(|buf| buf.lines.clone()); + b.set_redraw(true); + commands.entity(e).remove::<CosmicEditor>(); + } + } else if let Some(focused) = active_editor.0 { + for (e, mut b, ed) in q.iter_mut() { + if e != focused { + b.lines = ed.with_buffer(|buf| buf.lines.clone()); + b.set_redraw(true); + commands.entity(e).remove::<CosmicEditor>(); + } + } + } +} diff --git a/src/input.rs b/src/input.rs index 5d0192c02410a7268b2652607ef605e6a7eba527..5052197350ad62e46c0a5af8e40515745a518bdd 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,5 @@ #![allow(clippy::too_many_arguments, clippy::type_complexity)] -use std::time::Duration; - #[cfg(target_arch = "wasm32")] use bevy::tasks::AsyncComputeTaskPool; @@ -10,8 +8,10 @@ use bevy::{ prelude::*, window::PrimaryWindow, }; -use cosmic_text::{Action, AttrsList, BufferLine, Cursor, Edit, Shaping}; +use cosmic_text::{Action, Cursor, Edit, Motion, Selection}; +#[cfg(target_arch = "wasm32")] +use crate::DefaultAttrs; #[cfg(target_arch = "wasm32")] use js_sys::Promise; #[cfg(target_arch = "wasm32")] @@ -20,10 +20,10 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use crate::{ - get_node_cursor_pos, get_timestamp, get_x_offset_center, get_y_offset_center, - save_edit_history, CosmicAttrs, CosmicEditHistory, CosmicEditor, CosmicFontSystem, - CosmicMaxChars, CosmicMaxLines, CosmicSource, CosmicTextChanged, CosmicTextPosition, Focus, - PasswordInput, ReadOnly, XOffset, + buffer::{get_x_offset_center, get_y_offset_center}, + get_node_cursor_pos, CosmicBuffer, CosmicEditor, CosmicFontSystem, CosmicMaxChars, + CosmicMaxLines, CosmicSource, CosmicTextChanged, CosmicTextPosition, FocusedWidget, ReadOnly, + XOffset, }; #[derive(Resource)] @@ -44,7 +44,7 @@ pub struct WasmPasteAsyncChannel { pub(crate) fn input_mouse( windows: Query<&Window, With<PrimaryWindow>>, - active_editor: Res<Focus>, + active_editor: Res<FocusedWidget>, keys: Res<ButtonInput<KeyCode>>, buttons: Res<ButtonInput<MouseButton>>, mut editor_q: Query<( @@ -95,6 +95,8 @@ pub(crate) fn input_mouse( if let Ok((mut editor, sprite_transform, text_position, entity, x_offset, sprite)) = editor_q.get_mut(active_editor_entity) { + let buffer = editor.with_buffer(|b| b.clone()); + let mut is_ui_node = false; let mut transform = sprite_transform; let (mut width, mut height) = @@ -114,21 +116,21 @@ pub(crate) fn input_mouse( let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]); // if shift key is pressed - let already_has_selection = editor.0.select_opt().is_some(); + let already_has_selection = editor.selection() != Selection::None; if shift && !already_has_selection { - let cursor = editor.0.cursor(); - editor.0.set_select_opt(Some(cursor)); + let cursor = editor.cursor(); + editor.set_selection(Selection::Normal(cursor)); } let (padding_x, padding_y) = match text_position { CosmicTextPosition::Center => ( - get_x_offset_center(width * scale_factor, editor.0.buffer()), - get_y_offset_center(height * scale_factor, editor.0.buffer()), + get_x_offset_center(width * scale_factor, &buffer), + get_y_offset_center(height * scale_factor, &buffer), ), CosmicTextPosition::TopLeft { padding } => (*padding, *padding), CosmicTextPosition::Left { padding } => ( *padding, - get_y_offset_center(height * scale_factor, editor.0.buffer()), + get_y_offset_center(height * scale_factor, &buffer), ), }; let point = |node_cursor_pos: (f32, f32)| { @@ -139,6 +141,9 @@ pub(crate) fn input_mouse( }; if buttons.just_pressed(MouseButton::Left) { + editor.cursor_visible = true; + editor.cursor_timer.reset(); + if let Some(node_cursor_pos) = get_node_cursor_pos( primary_window, transform, @@ -150,25 +155,26 @@ pub(crate) fn input_mouse( 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 }); + editor.action(&mut font_system.0, Action::Drag { x, y }); } else { match *click_count { 1 => { - editor.0.action(&mut font_system.0, Action::Click { x, y }); + editor.action(&mut font_system.0, Action::Click { x, y }); } 2 => { // select word - editor.0.action(&mut font_system.0, Action::LeftWord); - let cursor = editor.0.cursor(); - editor.0.set_select_opt(Some(cursor)); - editor.0.action(&mut font_system.0, Action::RightWord); + editor.action(&mut font_system.0, Action::Motion(Motion::LeftWord)); + let cursor = editor.cursor(); + editor.set_selection(Selection::Normal(cursor)); + editor.action(&mut font_system.0, Action::Motion(Motion::RightWord)); } 3 => { // select paragraph - editor.0.action(&mut font_system.0, Action::ParagraphStart); - let cursor = editor.0.cursor(); - editor.0.set_select_opt(Some(cursor)); - editor.0.action(&mut font_system.0, Action::ParagraphEnd); + editor + .action(&mut font_system.0, Action::Motion(Motion::ParagraphStart)); + let cursor = editor.cursor(); + editor.set_selection(Selection::Normal(cursor)); + editor.action(&mut font_system.0, Action::Motion(Motion::ParagraphEnd)); } _ => {} } @@ -189,9 +195,9 @@ pub(crate) fn input_mouse( 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 }); + editor.action(&mut font_system.0, Action::Click { x, y }); } else { - editor.0.action(&mut font_system.0, Action::Drag { x, y }); + editor.action(&mut font_system.0, Action::Drag { x, y }); } } return; @@ -200,7 +206,7 @@ pub(crate) fn input_mouse( for ev in scroll_evr.read() { match ev.unit { MouseScrollUnit::Line => { - editor.0.action( + editor.action( &mut font_system.0, Action::Scroll { lines: -ev.y as i32, @@ -208,8 +214,8 @@ pub(crate) fn input_mouse( ); } MouseScrollUnit::Pixel => { - let line_height = editor.0.buffer().metrics().line_height; - editor.0.action( + let line_height = buffer.metrics().line_height; + editor.action( &mut font_system.0, Action::Scroll { lines: -(ev.y / line_height) as i32, @@ -221,15 +227,17 @@ pub(crate) fn input_mouse( } } +#[derive(Component)] +pub struct PasswordInput; // PLACEHOLDER bc this fn uses it's presence + // TODO: split copy/paste into own fn, separate fn for wasm pub(crate) fn input_kb( - active_editor: Res<Focus>, + active_editor: Res<FocusedWidget>, keys: Res<ButtonInput<KeyCode>>, mut char_evr: EventReader<ReceivedCharacter>, mut cosmic_edit_query: Query<( &mut CosmicEditor, - &mut CosmicEditHistory, - &CosmicAttrs, + &mut CosmicBuffer, &CosmicMaxLines, &CosmicMaxChars, Entity, @@ -239,30 +247,21 @@ pub(crate) fn input_kb( mut evw_changed: EventWriter<CosmicTextChanged>, mut font_system: ResMut<CosmicFontSystem>, mut is_deleting: Local<bool>, - mut edits_duration: Local<Option<Duration>>, _channel: Option<Res<WasmPasteAsyncChannel>>, ) { let Some(active_editor_entity) = active_editor.0 else { return; }; - if let Ok(( - mut editor, - mut edit_history, - attrs, - max_lines, - max_chars, - entity, - readonly_opt, - password_opt, - )) = cosmic_edit_query.get_mut(active_editor_entity) + if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt, password_opt)) = + cosmic_edit_query.get_mut(active_editor_entity) { + if keys.get_just_pressed().len() != 0 { + editor.cursor_visible = true; + editor.cursor_timer.reset(); + } let readonly = readonly_opt.is_some(); - let attrs = &attrs.0; - - let now_ms = get_timestamp(); - let command = keypress_command(&keys); #[cfg(target_arch = "wasm32")] @@ -284,10 +283,10 @@ pub(crate) fn input_kb( let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]); // if shift key is pressed - let already_has_selection = editor.0.select_opt().is_some(); + let already_has_selection = editor.selection() != Selection::None; if shift && !already_has_selection { - let cursor = editor.0.cursor(); - editor.0.set_select_opt(Some(cursor)); + let cursor = editor.cursor(); + editor.set_selection(Selection::Normal(cursor)); } #[cfg(target_os = "macos")] @@ -296,126 +295,124 @@ pub(crate) fn input_kb( let should_jump = command; if should_jump && keys.just_pressed(KeyCode::ArrowLeft) { - editor.0.action(&mut font_system.0, Action::PreviousWord); + editor.action(&mut font_system.0, Action::Motion(Motion::PreviousWord)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if should_jump && keys.just_pressed(KeyCode::ArrowRight) { - editor.0.action(&mut font_system.0, Action::NextWord); + editor.action(&mut font_system.0, Action::Motion(Motion::NextWord)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if should_jump && keys.just_pressed(KeyCode::Home) { - editor.0.action(&mut font_system.0, Action::BufferStart); - // there's a bug with cosmic text where it doesn't update the visual cursor for this action - // TODO: fix upstream - editor.0.buffer_mut().set_redraw(true); + editor.action(&mut font_system.0, Action::Motion(Motion::BufferStart)); + editor.set_redraw(true); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if should_jump && keys.just_pressed(KeyCode::End) { - editor.0.action(&mut font_system.0, Action::BufferEnd); - // there's a bug with cosmic text where it doesn't update the visual cursor for this action - // TODO: fix upstream - editor.0.buffer_mut().set_redraw(true); + editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd)); + editor.set_redraw(true); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::ArrowLeft) { - editor.0.action(&mut font_system.0, Action::Left); + editor.action(&mut font_system.0, Action::Motion(Motion::Left)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::ArrowRight) { - editor.0.action(&mut font_system.0, Action::Right); + editor.action(&mut font_system.0, Action::Motion(Motion::Right)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::ArrowUp) { - editor.0.action(&mut font_system.0, Action::Up); + editor.action(&mut font_system.0, Action::Motion(Motion::Up)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::ArrowDown) { - editor.0.action(&mut font_system.0, Action::Down); + editor.action(&mut font_system.0, Action::Motion(Motion::Down)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::Backspace) & !readonly { // fix for issue #8 - if let Some(select) = editor.0.select_opt() { - if editor.0.cursor().line == select.line && editor.0.cursor().index == select.index - { - editor.0.set_select_opt(None); - } + let select = editor.selection(); + if select != Selection::None { + // TODO: fix zero-width selections if needed + // + // if editor.cursor().line == select.line && editor.cursor().index == select.index { + // editor.set_selection(Selection::None); + // } } *is_deleting = true; - editor.0.action(&mut font_system.0, Action::Backspace); + #[cfg(target_arch = "wasm32")] + editor.action(&mut font_system.0, Action::Backspace); } if keys.just_released(KeyCode::Backspace) { *is_deleting = false; } if keys.just_pressed(KeyCode::Delete) && !readonly { - editor.0.action(&mut font_system.0, Action::Delete); + editor.action(&mut font_system.0, Action::Delete); } if keys.just_pressed(KeyCode::Escape) { - editor.0.action(&mut font_system.0, Action::Escape); + editor.action(&mut font_system.0, Action::Escape); } if command && keys.just_pressed(KeyCode::KeyA) { - editor.0.action(&mut font_system.0, Action::BufferEnd); - let current_cursor = editor.0.cursor(); - editor.0.set_select_opt(Some(Cursor { + editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd)); + let current_cursor = editor.cursor(); + editor.set_selection(Selection::Normal(Cursor { line: 0, index: 0, affinity: current_cursor.affinity, - color: current_cursor.color, })); return; } if keys.just_pressed(KeyCode::Home) { - editor.0.action(&mut font_system.0, Action::Home); + editor.action(&mut font_system.0, Action::Motion(Motion::Home)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::End) { - editor.0.action(&mut font_system.0, Action::End); + editor.action(&mut font_system.0, Action::Motion(Motion::End)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::PageUp) { - editor.0.action(&mut font_system.0, Action::PageUp); + editor.action(&mut font_system.0, Action::Motion(Motion::PageUp)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } if keys.just_pressed(KeyCode::PageDown) { - editor.0.action(&mut font_system.0, Action::PageDown); + editor.action(&mut font_system.0, Action::Motion(Motion::PageDown)); if !shift { - editor.0.set_select_opt(None); + editor.set_selection(Selection::None); } return; } @@ -428,7 +425,7 @@ pub(crate) fn input_kb( if password_opt.is_some() { return; } - if let Some(text) = editor.0.copy_selection() { + if let Some(text) = editor.copy_selection() { clipboard.set_text(text).unwrap(); return; } @@ -437,28 +434,26 @@ pub(crate) fn input_kb( if password_opt.is_some() { return; } - if let Some(text) = editor.0.copy_selection() { + if let Some(text) = editor.copy_selection() { clipboard.set_text(text).unwrap(); - editor.0.delete_selection(); + editor.delete_selection(); } is_clipboard = true; } if command && keys.just_pressed(KeyCode::KeyV) && !readonly { if let Ok(text) = clipboard.get_text() { for c in text.chars() { - if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 { + if max_chars.0 == 0 || buffer.get_text().len() < max_chars.0 { if c == 0xA as char { - if max_lines.0 == 0 - || editor.0.buffer().lines.len() < max_lines.0 - { - editor.0.action(&mut font_system.0, Action::Insert(c)); + if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 { + editor.action(&mut font_system.0, Action::Insert(c)); } } else { if password_opt.is_some() && c.len_utf8() > 1 { println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c); continue; } - editor.0.action(&mut font_system.0, Action::Insert(c)); + editor.action(&mut font_system.0, Action::Insert(c)); } } } @@ -474,7 +469,7 @@ pub(crate) fn input_kb( if password_opt.is_some() { return; } - if let Some(text) = editor.0.copy_selection() { + if let Some(text) = editor.copy_selection() { write_clipboard_wasm(text.as_str()); return; } @@ -484,9 +479,9 @@ pub(crate) fn input_kb( if password_opt.is_some() { return; } - if let Some(text) = editor.0.copy_selection() { + if let Some(text) = editor.copy_selection() { write_clipboard_wasm(text.as_str()); - editor.0.delete_selection(); + editor.delete_selection(); } is_clipboard = true; } @@ -516,12 +511,12 @@ pub(crate) fn input_kb( let mut is_return = false; if keys.just_pressed(KeyCode::Enter) { is_return = true; - if (max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0) - && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0) + if (max_lines.0 == 0 || buffer.lines.len() < max_lines.0) + && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0) { // to have new line on wasm rather than E is_edit = true; - editor.0.action(&mut font_system.0, Action::Insert('\n')); + editor.action(&mut font_system.0, Action::Insert('\n')); } } @@ -529,8 +524,8 @@ pub(crate) fn input_kb( for char_ev in char_evr.read() { is_edit = true; if *is_deleting { - editor.0.action(&mut font_system.0, Action::Backspace); - } else if !command && (max_chars.0 == 0 || editor.get_text().len() < max_chars.0) { + editor.action(&mut font_system.0, Action::Backspace); + } else if !command && (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0) { if password_opt.is_some() && char_ev.char.len() > 1 { println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",char_ev.char); continue; @@ -538,35 +533,18 @@ pub(crate) fn input_kb( let b = char_ev.char.as_bytes(); for c in b { let c: char = (*c).into(); - editor.0.action(&mut font_system.0, Action::Insert(c)); + editor.action(&mut font_system.0, Action::Insert(c)); + editor.set_redraw(true); } } } } - // skip event + history if undo/redo keys pressed - - let requested_redo = keypress_redo(&keys); - let requested_undo = - command && (keys.pressed(KeyCode::KeyZ) || keys.just_pressed(KeyCode::KeyZ)); - - if requested_redo || requested_undo || !is_edit { + if !is_edit { return; } - evw_changed.send(CosmicTextChanged((entity, editor.get_text()))); - - if let Some(last_edit_duration) = *edits_duration { - if Duration::from_millis(now_ms as u64) - last_edit_duration - > Duration::from_millis(150) - { - save_edit_history(&mut editor.0, attrs, &mut edit_history); - *edits_duration = Some(Duration::from_millis(now_ms as u64)); - } - } else { - save_edit_history(&mut editor.0, attrs, &mut edit_history); - *edits_duration = Some(Duration::from_millis(now_ms as u64)); - } + evw_changed.send(CosmicTextChanged((entity, buffer.get_text()))); } } @@ -593,96 +571,6 @@ fn keypress_command(keys: &ButtonInput<KeyCode>) -> bool { command } -fn keypress_redo(keys: &ButtonInput<KeyCode>) -> bool { - let command = keypress_command(keys); - let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]); - - #[cfg(not(target_os = "windows"))] - let requested_redo = command && shift && keys.just_pressed(KeyCode::KeyZ); - - // TODO: windows OS detection for wasm here - #[cfg(target_os = "windows")] - let requested_redo = command && keys.just_pressed(KeyCode::KeyY); - - requested_redo -} - -pub(crate) fn undo_redo( - active_editor: Res<Focus>, - keys: Res<ButtonInput<KeyCode>>, - mut editor_q: Query< - (&mut CosmicEditor, &CosmicAttrs, &mut CosmicEditHistory), - Without<ReadOnly>, - >, - mut evw_changed: EventWriter<CosmicTextChanged>, -) { - let entity = match active_editor.0 { - Some(entity) => entity, - None => return, - }; - - let (mut editor, attrs, mut edit_history) = match editor_q.get_mut(entity) { - Ok(components) => components, - Err(_) => return, - }; - - let command = keypress_command(&keys); - - let attrs = &attrs.0; - - let requested_redo = keypress_redo(&keys); - let requested_undo = command && keys.just_pressed(KeyCode::KeyZ); - - if !(requested_redo || requested_undo) { - return; - } - - let edits = &edit_history.edits; - - if edits.is_empty() { - return; - } - - // use not redo rather than undo, cos undo will be true when redo is - if !requested_redo && edit_history.current_edit == 0 { - return; - } - - if requested_redo && edit_history.current_edit == edits.len() - 1 { - return; - } - - let index = if requested_redo { - edit_history.current_edit + 1 - } else { - edit_history.current_edit - 1 - }; - - if let Some(current_edit) = edits.get(index) { - editor.0.buffer_mut().lines.clear(); - for line in current_edit.lines.iter() { - let mut line_text = String::new(); - let mut attrs_list = AttrsList::new(attrs.as_attrs()); - for (text, attrs) in line.iter() { - let start = line_text.len(); - line_text.push_str(text); - let end = line_text.len(); - attrs_list.add_span(start..end, attrs.as_attrs()); - } - editor.0.buffer_mut().lines.push(BufferLine::new( - line_text, - attrs_list, - Shaping::Advanced, - )); - } - editor.0.set_cursor(current_edit.cursor); - editor.0.set_select_opt(None); // prevent auto selection of redo-inserted text - editor.0.buffer_mut().set_redraw(true); - edit_history.current_edit = index; - evw_changed.send(CosmicTextChanged((entity, editor.get_text()))); - } -} - #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn write_clipboard_wasm(text: &str) { @@ -711,10 +599,10 @@ pub fn poll_wasm_paste( mut editor_q: Query< ( &mut CosmicEditor, - &CosmicAttrs, + &mut CosmicBuffer, + &crate::DefaultAttrs, &CosmicMaxChars, &CosmicMaxChars, - &mut CosmicEditHistory, Option<&PasswordInput>, ), Without<ReadOnly>, @@ -726,30 +614,28 @@ pub fn poll_wasm_paste( match inlet { Ok(inlet) => { let entity = inlet.entity; - if let Ok((mut editor, attrs, max_chars, max_lines, mut edit_history, password_opt)) = + if let Ok((mut editor, mut buffer, attrs, max_chars, max_lines, password_opt)) = editor_q.get_mut(entity) { let text = inlet.text; let attrs = &attrs.0; for c in text.chars() { - if max_chars.0 == 0 || editor.get_text().len() < max_chars.0 { + if max_chars.0 == 0 || buffer.get_text().len() < max_chars.0 { if c == 0xA as char { - if max_lines.0 == 0 || editor.0.buffer().lines.len() < max_lines.0 { - editor.0.action(&mut font_system.0, Action::Insert(c)); + if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 { + editor.action(&mut font_system.0, Action::Insert(c)); } } else { if password_opt.is_some() && c.len_utf8() > 1 { - // TODO: console.log here instead - println!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c); + info!("Cannot input multi-byte char '{}' to password field! See https://github.com/StaffEngineer/bevy_cosmic_edit/pull/99#issuecomment-1782607486",c); continue; } - editor.0.action(&mut font_system.0, Action::Insert(c)); + editor.action(&mut font_system.0, Action::Insert(c)); } } } - evw_changed.send(CosmicTextChanged((entity, editor.get_text()))); - save_edit_history(&mut editor.0, attrs, &mut edit_history); + evw_changed.send(CosmicTextChanged((entity, buffer.get_text()))); } } Err(_) => {} diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000000000000000000000000000000000000..2665d9ae1b0dcc12c28c6a6beb83b1bdd35960eb --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,171 @@ +use crate::*; +use bevy::{prelude::*, window::PrimaryWindow}; +use cosmic_text::Affinity; + +use self::buffer::{get_x_offset_center, get_y_offset_center}; + +#[derive(Component, Default)] +pub struct CosmicPadding(pub Vec2); + +#[derive(Component, Default)] +pub struct CosmicWidgetSize(pub Vec2); + +pub fn reshape(mut query: Query<&mut CosmicEditor>, mut font_system: ResMut<CosmicFontSystem>) { + for mut cosmic_editor in query.iter_mut() { + cosmic_editor.shape_as_needed(&mut font_system.0, false); + } +} + +pub fn set_padding( + mut query: Query< + ( + &mut CosmicPadding, + &CosmicTextPosition, + &CosmicBuffer, + &CosmicWidgetSize, + Option<&CosmicEditor>, + ), + Or<( + With<CosmicEditor>, + Changed<CosmicTextPosition>, + Changed<CosmicBuffer>, + Changed<CosmicWidgetSize>, + )>, + >, +) { + for (mut padding, position, buffer, size, editor_opt) in query.iter_mut() { + // TODO: At least one of these clones is uneccessary + let mut buffer = buffer.0.clone(); + + if let Some(editor) = editor_opt { + buffer = editor.with_buffer(|b| b.clone()); + } + + if !buffer.redraw() { + continue; + } + + padding.0 = match position { + CosmicTextPosition::Center => Vec2::new( + get_x_offset_center(size.0.x, &buffer) as f32, + get_y_offset_center(size.0.y, &buffer) as f32, + ), + CosmicTextPosition::TopLeft { padding } => Vec2::new(*padding as f32, *padding as f32), + CosmicTextPosition::Left { padding } => Vec2::new( + *padding as f32, + get_y_offset_center(size.0.y, &buffer) as f32, + ), + } + } +} + +pub fn set_widget_size( + mut query: Query<(&mut CosmicWidgetSize, &Sprite), Changed<Sprite>>, + windows: Query<&Window, With<PrimaryWindow>>, +) { + if windows.iter().len() == 0 { + return; + } + // TODO: early return if sprite size is unchanged + let scale = windows.single().scale_factor(); + for (mut size, sprite) in query.iter_mut() { + size.0 = sprite.custom_size.unwrap().ceil() * scale; + } +} + +pub fn set_buffer_size( + mut query: Query< + ( + &mut CosmicBuffer, + &CosmicMode, + &CosmicWidgetSize, + &CosmicTextPosition, + ), + Or<( + Changed<CosmicMode>, + Changed<CosmicWidgetSize>, + Changed<CosmicTextPosition>, + )>, + >, + mut font_system: ResMut<CosmicFontSystem>, +) { + for (mut buffer, mode, size, position) in query.iter_mut() { + let padding_x = match position { + CosmicTextPosition::Center => 0., + CosmicTextPosition::TopLeft { padding } => *padding as f32, + CosmicTextPosition::Left { padding } => *padding as f32, + }; + + let (buffer_width, buffer_height) = match mode { + CosmicMode::InfiniteLine => (f32::MAX, size.0.y), + CosmicMode::Wrap => (size.0.x - padding_x, size.0.y), + }; + + buffer.set_size(&mut font_system.0, buffer_width, buffer_height); + } +} + +pub fn new_image_from_default( + mut query: Query<&mut Handle<Image>, Added<CosmicBuffer>>, + mut images: ResMut<Assets<Image>>, +) { + for mut canvas in query.iter_mut() { + *canvas = images.add(Image::default()); + } +} + +pub fn set_cursor( + mut query: Query<( + &mut XOffset, + &CosmicMode, + &CosmicEditor, + &CosmicBuffer, + &CosmicWidgetSize, + &CosmicPadding, + )>, +) { + for (mut x_offset, mode, editor, buffer, size, padding) in query.iter_mut() { + let mut cursor_x = 0.; + if mode == &CosmicMode::InfiniteLine { + if let Some(line) = 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() { + *x_offset = XOffset(Some((0., size.0.x - 2. * padding.0.x))); + } + + 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))); + } + } + } +} + +pub fn set_sprite_size_from_ui( + mut source_q: Query<&mut Sprite, With<CosmicBuffer>>, + dest_q: Query<(&Node, &CosmicSource), Changed<Node>>, +) { + for (node, source) in dest_q.iter() { + if let Ok(mut sprite) = source_q.get_mut(source.0) { + sprite.custom_size = Some(node.size().ceil().max(Vec2::ONE)); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8bb9ec2fe0413aa8a91b1f7db056b7de78048dd7..0cc9cf3c8ddce3fbb9ce532758cc5ae45fecb383 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,51 +1,43 @@ #![allow(clippy::type_complexity)] +mod buffer; mod cursor; +pub mod focus; mod input; +mod layout; mod render; -use std::{collections::VecDeque, path::PathBuf}; +use std::{path::PathBuf, time::Duration}; use bevy::{prelude::*, transform::TransformSystem}; -pub use cosmic_text::{ - Action, Attrs, AttrsOwned, Color as CosmicColor, Cursor, Edit, Family, Style as FontStyle, - Weight as FontWeight, +use buffer::{ + add_font_system, set_editor_redraw, set_initial_scale, set_redraw, swap_target_handle, }; -use cosmic_text::{ - AttrsList, Buffer, BufferLine, Editor, FontSystem, Metrics, Shaping, SwashCache, +pub use buffer::{get_x_offset_center, get_y_offset_center, CosmicBuffer}; +pub use cosmic_text::{ + Action, Attrs, AttrsOwned, Color as CosmicColor, Cursor, Edit, Family, Metrics, Shaping, + Style as FontStyle, Weight as FontWeight, }; +use cosmic_text::{Buffer, Editor, FontSystem, SwashCache}; use cursor::{change_cursor, hover_sprites, hover_ui}; pub use cursor::{TextHoverIn, TextHoverOut}; -use input::{input_kb, input_mouse, undo_redo, ClickTimer}; +pub use focus::*; +use input::{input_kb, input_mouse, ClickTimer}; #[cfg(target_arch = "wasm32")] use input::{poll_wasm_paste, WasmPaste, WasmPasteAsyncChannel}; -use render::{ - blink_cursor, freeze_cursor_blink, hide_inactive_or_readonly_cursor, hide_password_text, - on_scale_factor_change, restore_password_text, restore_placeholder_text, set_initial_scale, - show_placeholder, CosmicPadding, CosmicRenderSet, CosmicWidgetSize, CursorBlinkTimer, - CursorVisibility, PasswordValues, SwashCacheState, +use layout::{ + new_image_from_default, reshape, set_buffer_size, set_cursor, set_padding, + set_sprite_size_from_ui, set_widget_size, CosmicPadding, CosmicWidgetSize, }; +use render::{blink_cursor, render_texture, SwashCacheState}; #[cfg(feature = "multicam")] #[derive(Component)] pub struct CosmicPrimaryCamera; -#[derive(Clone, Component, PartialEq, Debug)] -pub enum CosmicText { - OneStyle(String), - MultiStyle(Vec<Vec<(String, AttrsOwned)>>), -} - -impl Default for CosmicText { - fn default() -> Self { - Self::OneStyle(String::new()) - } -} - #[derive(Clone, Component, PartialEq, Default)] pub enum CosmicMode { InfiniteLine, - AutoHeight, #[default] Wrap, } @@ -74,151 +66,59 @@ pub enum CosmicTextPosition { #[derive(Event, Debug)] pub struct CosmicTextChanged(pub (Entity, String)); -// TODO docs -const DEFAULT_SCALE_PLACEHOLDER: f32 = 0.696969; - -#[derive(Clone, Component)] -pub struct CosmicMetrics { - pub font_size: f32, - pub line_height: f32, - pub scale_factor: f32, -} - -impl Default for CosmicMetrics { - fn default() -> Self { - Self { - font_size: 12., - line_height: 12., - scale_factor: DEFAULT_SCALE_PLACEHOLDER, - } - } -} - -#[derive(Resource)] +#[derive(Resource, Deref, DerefMut)] pub struct CosmicFontSystem(pub FontSystem); #[derive(Component)] pub struct ReadOnly; // tag component -#[derive(Component, Debug)] -struct XOffset(Option<(f32, f32)>); +#[derive(Component, Debug, Default)] +pub struct XOffset(Option<(f32, f32)>); -#[derive(Component)] -pub struct CosmicEditor(pub Editor); +#[derive(Component, Deref, DerefMut)] +pub struct CosmicEditor { + #[deref] + pub editor: Editor<'static>, + pub cursor_visible: bool, + pub cursor_timer: Timer, +} impl CosmicEditor { - pub fn set_text( - &mut self, - text: CosmicText, - attrs: AttrsOwned, - font_system: &mut FontSystem, - ) -> &mut Self { - // TODO: invoke trim_text here - let editor = &mut self.0; - editor.buffer_mut().lines.clear(); - match text { - CosmicText::OneStyle(text) => { - editor.buffer_mut().set_text( - font_system, - text.as_str(), - attrs.as_attrs(), - Shaping::Advanced, - ); - let mut cursor = editor.cursor(); - cursor.line = editor.buffer_mut().lines.len() - 1; - cursor.index = editor.buffer_mut().lines[cursor.line].text().len(); - editor.set_cursor(cursor); - } - CosmicText::MultiStyle(lines) => { - for line in lines { - let mut line_text = String::new(); - let mut attrs_list = AttrsList::new(attrs.as_attrs()); - for (text, attrs) in line.iter() { - let start = line_text.len(); - line_text.push_str(text); - let end = line_text.len(); - attrs_list.add_span(start..end, attrs.as_attrs()); - } - editor.buffer_mut().lines.push(BufferLine::new( - line_text, - attrs_list, - Shaping::Advanced, - )); - } - } + fn new(editor: Editor<'static>) -> Self { + Self { + editor, + cursor_visible: true, + cursor_timer: Timer::new(Duration::from_millis(530), TimerMode::Repeating), } - self - } - - /// Retrieves the cosmic text content from an editor. - /// - /// # Arguments - /// - /// * none, takes the rust magic ref to self - /// - /// # Returns - /// - /// A `String` containing the cosmic text content. - pub fn get_text(&self) -> String { - let buffer = self.0.buffer(); - let mut text = String::new(); - let line_count = buffer.lines.len(); - - for (i, line) in buffer.lines.iter().enumerate() { - text.push_str(line.text()); - - if i < line_count - 1 { - text.push('\n'); - } - } - - text } } -#[derive(Component)] -pub struct CosmicAttrs(pub AttrsOwned); +#[derive(Component, Deref, DerefMut)] +pub struct DefaultAttrs(pub AttrsOwned); -impl Default for CosmicAttrs { +impl Default for DefaultAttrs { fn default() -> Self { - CosmicAttrs(AttrsOwned::new(Attrs::new())) + DefaultAttrs(AttrsOwned::new(Attrs::new())) } } #[derive(Component, Default)] pub struct CosmicBackground(pub Option<Handle<Image>>); -#[derive(Component, Default)] -pub struct CosmicMaxLines(pub usize); - -#[derive(Component, Default)] -pub struct CosmicMaxChars(pub usize); - -#[derive(Component, Default)] +#[derive(Component, Default, Deref)] pub struct FillColor(pub Color); -#[derive(Component, Default)] -pub struct PlaceholderText(pub CosmicText); +#[derive(Component, Default, Deref)] +pub struct CursorColor(pub Color); -#[derive(Component)] -pub struct PlaceholderAttrs(pub AttrsOwned); +#[derive(Component, Default, Deref)] +pub struct SelectionColor(pub Color); -impl Default for PlaceholderAttrs { - fn default() -> Self { - Self(AttrsOwned::new( - Attrs::new().color(CosmicColor::rgb(128, 128, 128)), - )) - } -} - -#[derive(Component)] -pub struct PasswordInput(pub char); +#[derive(Component, Default)] +pub struct CosmicMaxLines(pub usize); -impl Default for PasswordInput { - fn default() -> Self { - PasswordInput("鈥�".chars().next().unwrap()) - } -} +#[derive(Component, Default)] +pub struct CosmicMaxChars(pub usize); #[derive(Component)] pub struct CosmicSource(pub Entity); @@ -226,17 +126,21 @@ pub struct CosmicSource(pub Entity); #[derive(Bundle)] pub struct CosmicEditBundle { // cosmic bits + pub buffer: CosmicBuffer, + // render bits pub fill_color: FillColor, - pub text_position: CosmicTextPosition, - pub metrics: CosmicMetrics, - pub attrs: CosmicAttrs, + pub cursor_color: CursorColor, + pub selection_color: SelectionColor, + pub default_attrs: DefaultAttrs, pub background_image: CosmicBackground, + pub sprite_bundle: SpriteBundle, + // restriction bits pub max_lines: CosmicMaxLines, pub max_chars: CosmicMaxChars, - pub text_setter: CosmicText, + // layout bits + pub x_offset: XOffset, pub mode: CosmicMode, - pub sprite_bundle: SpriteBundle, - // render bits + pub text_position: CosmicTextPosition, pub padding: CosmicPadding, pub widget_size: CosmicWidgetSize, } @@ -244,14 +148,15 @@ pub struct CosmicEditBundle { impl Default for CosmicEditBundle { fn default() -> Self { CosmicEditBundle { + buffer: Default::default(), fill_color: Default::default(), + cursor_color: CursorColor(Color::BLACK), + selection_color: SelectionColor(Color::GRAY), text_position: Default::default(), - metrics: Default::default(), - attrs: Default::default(), + default_attrs: Default::default(), background_image: Default::default(), max_lines: Default::default(), max_chars: Default::default(), - text_setter: Default::default(), mode: Default::default(), sprite_bundle: SpriteBundle { sprite: Sprite { @@ -261,35 +166,13 @@ impl Default for CosmicEditBundle { visibility: Visibility::Hidden, ..default() }, + x_offset: XOffset(None), padding: Default::default(), widget_size: Default::default(), } } } -#[derive(Bundle)] -pub struct CosmicEditPlaceholderBundle { - /// set this to update placeholder text - pub text_setter: PlaceholderText, - pub attrs: PlaceholderAttrs, -} - -#[derive(Clone)] -pub struct EditHistoryItem { - pub cursor: Cursor, - pub lines: Vec<Vec<(String, AttrsOwned)>>, -} - -#[derive(Component, Default)] -pub struct CosmicEditHistory { - pub edits: VecDeque<EditHistoryItem>, - pub current_edit: usize, -} - -/// Resource struct that keeps track of the currently active editor entity. -#[derive(Resource, Default, Deref, DerefMut)] -pub struct Focus(pub Option<Entity>); - /// Resource struct that holds configuration options for cosmic fonts. #[derive(Resource, Clone)] pub struct CosmicFontConfig { @@ -302,7 +185,7 @@ impl Default for CosmicFontConfig { fn default() -> Self { let fallback_font = include_bytes!("./font/FiraMono-Regular-subset.ttf"); Self { - load_system_fonts: false, + load_system_fonts: true, font_bytes: Some(vec![fallback_font]), fonts_dir_path: None, } @@ -320,80 +203,40 @@ impl Plugin for CosmicEditPlugin { fn build(&self, app: &mut App) { let font_system = create_cosmic_font_system(self.font_config.clone()); - let main_unordered = ( - blink_cursor, - freeze_cursor_blink, - hide_inactive_or_readonly_cursor, - clear_inactive_selection, - ); - - let render_systems = ( - render::new_image_from_default.in_set(CosmicRenderSet::Setup), - render::set_size_from_ui.in_set(CosmicRenderSet::Setup), - render::cosmic_reshape.in_set(CosmicRenderSet::Shaping), - render::cosmic_widget_size.in_set(CosmicRenderSet::Sizing), - render::cosmic_buffer_size.in_set(CosmicRenderSet::Sizing), - render::auto_height - .after(CosmicRenderSet::Sizing) - .before(CosmicRenderSet::Draw), - render::cosmic_padding.in_set(CosmicRenderSet::Padding), - render::set_cursor.in_set(CosmicRenderSet::Cursor), - render::render_texture.in_set(CosmicRenderSet::Draw), - ); + let layout_systems = ( + (new_image_from_default, set_sprite_size_from_ui), + set_widget_size, + set_buffer_size, + set_padding, + set_cursor, + ) + .chain(); app.add_systems( First, ( + add_font_system, set_initial_scale, - (cosmic_editor_builder, on_scale_factor_change).after(set_initial_scale), - render::swap_target_handle, - ), - ) - .add_systems( - PreUpdate, - ( - update_buffer_text, - init_history, - main_unordered, - hide_password_text, - input_mouse, - restore_password_text, + set_redraw, + set_editor_redraw, + swap_target_handle, ) .chain(), ) - .add_systems(Update, (input_kb, undo_redo).chain()) - .configure_sets( + .add_systems(PreUpdate, (input_mouse,).chain()) + .add_systems(Update, (input_kb, reshape, blink_cursor).chain()) + .add_systems( PostUpdate, ( - CosmicRenderSet::Setup, - CosmicRenderSet::Shaping, - CosmicRenderSet::Sizing, - CosmicRenderSet::Cursor, - CosmicRenderSet::Padding, - CosmicRenderSet::Draw, + layout_systems, + drop_editor_unfocused, + add_editor_to_focused, + render_texture, ) .chain() .after(TransformSystem::TransformPropagate), ) - .add_systems( - PostUpdate, - ( - hide_password_text, - show_placeholder, - render_systems, - apply_deferred, // Prevents one-frame inputs adding placeholder to editor - restore_password_text, - restore_placeholder_text, - ) - .chain(), - ) - .init_resource::<Focus>() - .init_resource::<PasswordValues>() - .insert_resource(CursorBlinkTimer(Timer::from_seconds( - 0.53, - TimerMode::Repeating, - ))) - .insert_resource(CursorVisibility(true)) + .init_resource::<FocusedWidget>() .insert_resource(SwashCacheState { swash_cache: SwashCache::new(), }) @@ -424,64 +267,6 @@ impl Plugin for CosmicEditPlugin { } } -fn save_edit_history( - editor: &mut Editor, - attrs: &AttrsOwned, - edit_history: &mut CosmicEditHistory, -) { - let edits = &edit_history.edits; - let current_lines = get_text_spans(editor.buffer(), attrs.clone()); - let current_edit = edit_history.current_edit; - let mut new_edits = VecDeque::new(); - new_edits.extend(edits.iter().take(current_edit + 1).cloned()); - // remove old edits - if new_edits.len() > 1000 { - new_edits.drain(0..100); - } - new_edits.push_back(EditHistoryItem { - cursor: editor.cursor(), - lines: current_lines, - }); - let len = new_edits.len(); - *edit_history = CosmicEditHistory { - edits: new_edits, - current_edit: len - 1, - }; -} - -fn init_history( - mut q: Query<(&mut CosmicEditor, &CosmicAttrs, &mut CosmicEditHistory), Added<CosmicEditor>>, -) { - for (mut editor, attrs, mut history) in q.iter_mut() { - save_edit_history(&mut editor.0, &attrs.0, &mut history); - } -} - -/// Adds the font system to each editor when added -fn cosmic_editor_builder( - mut added_editors: Query<(Entity, &CosmicMetrics), Added<CosmicText>>, - mut font_system: ResMut<CosmicFontSystem>, - mut commands: Commands, -) { - for (entity, metrics) in added_editors.iter_mut() { - let mut 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); - buffer.set_redraw(true); - let mut editor = Editor::new(buffer); - - let mut cursor = editor.cursor(); - cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0)); - editor.set_cursor(cursor); - - commands.entity(entity).insert(CosmicEditor(editor)); - commands.entity(entity).insert(CosmicEditHistory::default()); - commands.entity(entity).insert(XOffset(None)); - } -} - fn create_cosmic_font_system(cosmic_font_config: CosmicFontConfig) -> FontSystem { let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); let mut db = cosmic_text::fontdb::Database::new(); @@ -535,185 +320,6 @@ pub fn get_node_cursor_pos( }) } -/// Updates editor buffer when text component changes -fn update_buffer_text( - mut editor_q: Query< - ( - &mut CosmicEditor, - &mut CosmicText, - &CosmicAttrs, - &CosmicMaxChars, - &CosmicMaxLines, - ), - Changed<CosmicText>, - >, - mut font_system: ResMut<CosmicFontSystem>, -) { - for (mut editor, text, attrs, max_chars, max_lines) in editor_q.iter_mut() { - let text = trim_text(text.to_owned(), max_chars.0, max_lines.0); - editor.set_text(text, attrs.0.clone(), &mut font_system.0); - } -} - -fn trim_text(text: CosmicText, max_chars: usize, max_lines: usize) -> CosmicText { - if max_chars == 0 && max_lines == 0 { - // no limits, no work to do - return text; - } - - match text { - CosmicText::OneStyle(mut string) => { - if max_chars != 0 { - string.truncate(max_chars); - } - - if max_lines == 0 { - return CosmicText::OneStyle(string); - } - - let mut line_acc = 0; - let mut char_pos = 0; - for c in string.chars() { - char_pos += 1; - if c == 0xA as char { - line_acc += 1; - if line_acc >= max_lines { - // break text to pos - string.truncate(char_pos); - break; - } - } - } - - CosmicText::OneStyle(string) - } - CosmicText::MultiStyle(lines) => { - let mut char_acc = 0; - let mut line_acc = 0; - - let mut trimmed_styles = vec![]; - - for line in lines.iter() { - line_acc += 1; - char_acc += 1; // count newlines for consistent behaviour - - if (line_acc >= max_lines && max_lines > 0) - || (char_acc >= max_chars && max_chars > 0) - { - break; - } - - let mut strs = vec![]; - - for (string, attrs) in line.iter() { - if char_acc >= max_chars && max_chars > 0 { - break; - } - - let mut string = string.clone(); - - if max_chars > 0 { - string.truncate(max_chars - char_acc); - char_acc += string.len(); - } - - if max_lines > 0 { - for c in string.chars() { - if c == 0xA as char { - line_acc += 1; - char_acc += 1; // count newlines for consistent behaviour - if line_acc >= max_lines { - break; - } - } - } - } - - strs.push((string, attrs.clone())); - } - trimmed_styles.push(strs); - } - CosmicText::MultiStyle(trimmed_styles) - } - } -} -/// Returns texts from a MultiStyle buffer -pub fn get_text_spans( - buffer: &Buffer, - default_attrs: AttrsOwned, -) -> Vec<Vec<(String, AttrsOwned)>> { - let mut spans = Vec::new(); - for line in buffer.lines.iter() { - let mut line_spans = Vec::new(); - let line_text = line.text(); - let line_attrs = line.attrs_list(); - if line_attrs.spans().is_empty() { - line_spans.push((line_text.to_string(), default_attrs.clone())); - } else { - let mut current_pos = 0; - for span in line_attrs.spans() { - let span_range = span.0; - let span_attrs = span.1.clone(); - let start_index = span_range.start; - let end_index = span_range.end; - if start_index > current_pos { - // Add the text between the current position and the start of the span - let non_span_text = line_text[current_pos..start_index].to_string(); - line_spans.push((non_span_text, default_attrs.clone())); - } - let span_text = line_text[start_index..end_index].to_string(); - line_spans.push((span_text.clone(), span_attrs)); - current_pos = end_index; - } - if current_pos < line_text.len() { - // Add the remaining text after the last span - let remaining_text = line_text[current_pos..].to_string(); - line_spans.push((remaining_text, default_attrs.clone())); - } - } - spans.push(line_spans); - } - spans -} - -fn get_text_size(buffer: &Buffer) -> (f32, f32) { - if buffer.layout_runs().count() == 0 { - return (0., buffer.metrics().line_height); - } - let width = buffer - .layout_runs() - .map(|run| run.line_w) - .reduce(f32::max) - .unwrap(); - let height = buffer.layout_runs().count() as f32 * buffer.metrics().line_height; - (width, height) -} - -pub fn get_y_offset_center(widget_height: f32, buffer: &Buffer) -> i32 { - let (_, text_height) = get_text_size(buffer); - ((widget_height - text_height) / 2.0) as i32 -} - -pub fn get_x_offset_center(widget_width: f32, buffer: &Buffer) -> i32 { - let (text_width, _) = get_text_size(buffer); - ((widget_width - text_width) / 2.0) as i32 -} - -fn clear_inactive_selection( - mut cosmic_editor_q: Query<(Entity, &mut CosmicEditor)>, - active_editor: Res<Focus>, -) { - if !active_editor.is_changed() || active_editor.0.is_none() { - return; - } - - for (e, mut editor) in &mut cosmic_editor_q.iter_mut() { - if e != active_editor.0.unwrap() { - editor.0.set_select_opt(None); - } - } -} - #[cfg(target_arch = "wasm32")] pub fn get_timestamp() -> f64 { js_sys::Date::now() @@ -731,9 +337,19 @@ pub fn get_timestamp() -> f64 { mod tests { use crate::*; - fn test_spawn_cosmic_edit_system(mut commands: Commands) { + use self::buffer::CosmicBuffer; + + fn test_spawn_cosmic_edit_system( + mut commands: Commands, + mut font_system: ResMut<CosmicFontSystem>, + ) { + let attrs = Attrs::new(); commands.spawn(CosmicEditBundle { - text_setter: CosmicText::OneStyle("Blah".into()), + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(20., 20.)).with_rich_text( + &mut font_system, + vec![("Blah", attrs)], + attrs, + ), ..Default::default() }); } @@ -757,11 +373,9 @@ mod tests { app.update(); - let mut text_nodes_query = app.world.query::<&CosmicEditor>(); + let mut text_nodes_query = app.world.query::<&CosmicBuffer>(); for cosmic_editor in text_nodes_query.iter(&app.world) { insta::assert_debug_snapshot!(cosmic_editor - .0 - .buffer() .lines .iter() .map(|line| line.text()) diff --git a/src/render.rs b/src/render.rs index 8e6fac489455cd3037350961eacf8774c3a3c912..730d8c08d4680f641f6ea2d58eedd6dd8c4c53a0 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,151 +1,102 @@ -use std::time::Duration; - -use bevy::{ - prelude::*, - render::render_resource::Extent3d, - utils::HashMap, - window::{PrimaryWindow, WindowScaleFactorChanged}, -}; -use cosmic_text::{Affinity, Edit, Metrics, SwashCache}; +use bevy::{prelude::*, render::render_resource::Extent3d}; +use cosmic_text::{Color, Edit, SwashCache}; use image::{imageops::FilterType, GenericImageView}; use crate::{ - get_text_size, get_x_offset_center, get_y_offset_center, CosmicAttrs, CosmicBackground, - CosmicEditor, CosmicFontSystem, CosmicMetrics, CosmicMode, CosmicSource, CosmicText, - CosmicTextPosition, FillColor, Focus, PasswordInput, PlaceholderAttrs, PlaceholderText, - ReadOnly, XOffset, DEFAULT_SCALE_PLACEHOLDER, + layout::{CosmicPadding, CosmicWidgetSize}, + CosmicBackground, CosmicBuffer, CosmicEditor, CosmicFontSystem, CursorColor, DefaultAttrs, + FillColor, ReadOnly, SelectionColor, XOffset, }; -#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] -pub enum CosmicRenderSet { - Setup, - Shaping, - Sizing, - Cursor, - Padding, - Draw, -} - #[derive(Resource)] pub(crate) struct SwashCacheState { pub swash_cache: SwashCache, } -#[derive(Resource)] -pub(crate) struct CursorBlinkTimer(pub Timer); - -#[derive(Resource)] -pub(crate) struct CursorVisibility(pub bool); - -#[derive(Resource, Default)] -pub(crate) struct PasswordValues(pub HashMap<Entity, (String, usize)>); - -#[derive(Component)] -pub(crate) struct Placeholder; - -#[derive(Component, Default)] -pub struct CosmicPadding(pub Vec2); - -#[derive(Component, Default)] -pub struct CosmicWidgetSize(pub Vec2); - -pub(crate) fn cosmic_padding( - mut query: Query<( - &mut CosmicPadding, - &CosmicTextPosition, - &CosmicEditor, - &CosmicWidgetSize, - )>, -) { - for (mut padding, position, editor, size) in query.iter_mut() { - padding.0 = match position { - CosmicTextPosition::Center => Vec2::new( - get_x_offset_center(size.0.x, editor.0.buffer()) as f32, - get_y_offset_center(size.0.y, editor.0.buffer()) as f32, - ), - CosmicTextPosition::TopLeft { padding } => Vec2::new(*padding as f32, *padding as f32), - CosmicTextPosition::Left { padding } => Vec2::new( - *padding as f32, - get_y_offset_center(size.0.y, editor.0.buffer()) as f32, - ), +pub fn blink_cursor(mut q: Query<&mut CosmicEditor, Without<ReadOnly>>, time: Res<Time>) { + for mut e in q.iter_mut() { + e.cursor_timer.tick(time.delta()); + if e.cursor_timer.just_finished() { + e.cursor_visible = !e.cursor_visible; + e.set_redraw(true); } } } -pub(crate) fn cosmic_widget_size( - mut query: Query<(&mut CosmicWidgetSize, &Sprite), Changed<Sprite>>, - windows: Query<&Window, With<PrimaryWindow>>, -) { - if windows.iter().len() == 0 { +fn draw_pixel(buffer: &mut [u8], width: i32, height: i32, x: i32, y: i32, color: Color) { + let a_a = color.a() as u32; + if a_a == 0 { + // Do not draw if alpha is zero return; } - let scale = windows.single().scale_factor(); - for (mut size, sprite) in query.iter_mut() { - size.0 = sprite.custom_size.unwrap().ceil() * scale; + + if y < 0 || y >= height { + // Skip if y out of bounds + return; } -} -pub(crate) fn cosmic_buffer_size( - mut query: Query<( - &mut CosmicEditor, - &CosmicMode, - &CosmicWidgetSize, - &CosmicTextPosition, - )>, - mut font_system: ResMut<CosmicFontSystem>, -) { - for (mut editor, mode, size, position) in query.iter_mut() { - let padding_x = match position { - CosmicTextPosition::Center => 0., - CosmicTextPosition::TopLeft { padding } => *padding as f32, - CosmicTextPosition::Left { padding } => *padding as f32, - }; + if x < 0 || x >= width { + // Skip if x out of bounds + return; + } - let (buffer_width, buffer_height) = match mode { - CosmicMode::InfiniteLine => (f32::MAX, size.0.y), - CosmicMode::AutoHeight => (size.0.x - padding_x, (i32::MAX / 2) as f32), - CosmicMode::Wrap => (size.0.x - padding_x, size.0.y), - }; + let offset = (y as usize * width as usize + x as usize) * 4; - editor - .0 - .buffer_mut() - .set_size(&mut font_system.0, buffer_width, buffer_height); - } -} + let bg = bevy::prelude::Color::rgba_u8( + buffer[offset], + buffer[offset + 1], + buffer[offset + 2], + buffer[offset + 3], + ); -pub(crate) fn cosmic_reshape( - mut query: Query<&mut CosmicEditor>, - mut font_system: ResMut<CosmicFontSystem>, -) { - for mut cosmic_editor in query.iter_mut() { - cosmic_editor.0.shape_as_needed(&mut font_system.0); - } + // TODO: if alpha is 100% or bg is empty skip blending + + let fg = bevy::prelude::Color::rgba_u8(color.r(), color.g(), color.b(), color.a()); + + let premul = fg * Vec3::splat(color.a() as f32 / 255.0); + + let out = premul + bg * (1.0 - fg.a()); + + buffer[offset + 2] = (out.b() * 255.0) as u8; + buffer[offset + 1] = (out.g() * 255.0) as u8; + buffer[offset] = (out.r() * 255.0) as u8; + buffer[offset + 3] = (out.a() * 255.0) as u8; } pub(crate) fn render_texture( mut query: Query<( - &mut CosmicEditor, - &CosmicAttrs, + Option<&mut CosmicEditor>, + &mut CosmicBuffer, + &DefaultAttrs, &CosmicBackground, &FillColor, + &CursorColor, + &SelectionColor, &Handle<Image>, &CosmicWidgetSize, &CosmicPadding, &XOffset, + Option<&ReadOnly>, )>, mut font_system: ResMut<CosmicFontSystem>, mut images: ResMut<Assets<Image>>, mut swash_cache_state: ResMut<SwashCacheState>, ) { - for (mut cosmic_editor, attrs, background_image, fill_color, canvas, size, padding, x_offset) in - query.iter_mut() + for ( + editor, + mut buffer, + attrs, + background_image, + fill_color, + cursor_color, + selection_color, + canvas, + size, + padding, + x_offset, + readonly_opt, + ) in query.iter_mut() { - // TODO: redraw tag component - if !cosmic_editor.0.buffer().redraw() { - continue; - } - // Draw background let mut pixels = vec![0; size.0.x as usize * size.0.y as usize * 4]; if let Some(bg_image) = background_image.0.clone() { @@ -182,476 +133,77 @@ pub(crate) fn render_texture( .color_opt .unwrap_or(cosmic_text::Color::rgb(0, 0, 0)); - // Draw glyphs - cosmic_editor.0.draw( - &mut font_system.0, - &mut swash_cache_state.swash_cache, - font_color, - |x, y, w, h, color| { - for row in 0..h as i32 { - for col in 0..w as i32 { - draw_pixel( - &mut pixels, - size.0.x as i32, - size.0.y as i32, - x + col + padding.0.x as i32 - x_offset.0.unwrap_or((0., 0.)).0 as i32, - y + row + padding.0.y as i32, - color, - ); - } - } - }, - ); - - if let Some(prev_image) = images.get_mut(canvas) { - prev_image.data.clear(); - prev_image.data.extend_from_slice(pixels.as_slice()); - prev_image.resize(Extent3d { - width: size.0.x as u32, - height: size.0.y as u32, - depth_or_array_layers: 1, - }); - } - - cosmic_editor.0.buffer_mut().set_redraw(false); - } -} - -pub(crate) fn new_image_from_default( - mut query: Query<&mut Handle<Image>, Added<CosmicEditor>>, - mut images: ResMut<Assets<Image>>, -) { - for mut canvas in query.iter_mut() { - *canvas = images.add(Image::default()); - } -} - -pub(crate) fn set_cursor( - mut query: Query<( - &mut XOffset, - &CosmicMode, - &CosmicEditor, - &CosmicWidgetSize, - &CosmicPadding, - )>, -) { - for (mut x_offset, mode, cosmic_editor, size, padding) in query.iter_mut() { - let mut cursor_x = 0.; - if mode == &CosmicMode::InfiniteLine { - if let Some(line) = cosmic_editor.0.buffer().layout_runs().next() { - for (idx, glyph) in line.glyphs.iter().enumerate() { - if cosmic_editor.0.cursor().affinity == Affinity::Before { - if idx <= cosmic_editor.0.cursor().index { - cursor_x += glyph.w; - } - } else if idx < cosmic_editor.0.cursor().index { - cursor_x += glyph.w; - } else { - break; - } - } - } - } - - if mode == &CosmicMode::InfiniteLine && x_offset.0.is_none() { - *x_offset = XOffset(Some((0., size.0.x - 2. * padding.0.x))); - } - - 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))); - } - } - } -} - -pub(crate) fn auto_height( - mut query: Query<( - Entity, - &mut Sprite, - &CosmicMode, - &mut CosmicEditor, - &CosmicWidgetSize, - )>, - mut style_q: Query<(&mut Style, &CosmicSource)>, - windows: Query<&Window, With<PrimaryWindow>>, -) { - if windows.iter().len() == 0 { - return; - } - - let scale = windows.single().scale_factor(); - - for (entity, mut sprite, mode, mut cosmic_editor, size) in query.iter_mut() { - if mode == &CosmicMode::AutoHeight { - let text_size = get_text_size(cosmic_editor.0.buffer()); - let text_height = (text_size.1 + 30.) / scale; - if text_height > size.0.y / scale { - let mut new_size = sprite.custom_size.unwrap(); - new_size.y = text_height.ceil(); - // TODO this gets set automatically in UI cases but needs to be done for all other cases. - // redundant work but easier to just set on all sprites - sprite.custom_size = Some(new_size); - - cosmic_editor.0.buffer_mut().set_redraw(true); - - // TODO: bad loop nesting - for (mut style, source) in style_q.iter_mut() { - if source.0 != entity { - continue; - } - style.height = Val::Px(text_height.ceil()); + let draw_closure = |x, y, w, h, color| { + for row in 0..h as i32 { + for col in 0..w as i32 { + draw_pixel( + &mut pixels, + size.0.x as i32, + size.0.y as i32, + x + col + padding.0.x as i32 - x_offset.0.unwrap_or((0., 0.)).0 as i32, + y + row + padding.0.y as i32, + color, + ); } } - } - } -} - -pub(crate) fn set_size_from_ui( - mut source_q: Query<&mut Sprite, With<CosmicEditor>>, - dest_q: Query<(&Node, &CosmicSource)>, -) { - for (node, source) in dest_q.iter() { - if let Ok(mut sprite) = source_q.get_mut(source.0) { - sprite.custom_size = Some(node.size().ceil().max(Vec2::ONE)); - } - } -} - -pub(crate) fn _set_size_from_mesh() { - // TODO -} - -fn draw_pixel( - buffer: &mut [u8], - width: i32, - height: i32, - x: i32, - y: i32, - color: cosmic_text::Color, -) { - // TODO: perftest this fn against previous iteration - let a_a = color.a() as u32; - if a_a == 0 { - // Do not draw if alpha is zero - return; - } - - if y < 0 || y >= height { - // Skip if y out of bounds - return; - } - - if x < 0 || x >= width { - // Skip if x out of bounds - return; - } - - let offset = (y as usize * width as usize + x as usize) * 4; - - let bg = Color::rgba_u8( - buffer[offset], - buffer[offset + 1], - buffer[offset + 2], - buffer[offset + 3], - ); - - // TODO: if alpha is 100% or bg is empty skip blending - - let fg = Color::rgba_u8(color.r(), color.g(), color.b(), color.a()); - - let premul = fg * Vec3::splat(color.a() as f32 / 255.0); - - let out = premul + bg * (1.0 - fg.a()); - - buffer[offset + 2] = (out.b() * 255.0) as u8; - buffer[offset + 1] = (out.g() * 255.0) as u8; - buffer[offset] = (out.r() * 255.0) as u8; - buffer[offset + 3] = (out.a() * 255.0) as u8; -} - -pub(crate) fn blink_cursor( - mut visibility: ResMut<CursorVisibility>, - mut timer: ResMut<CursorBlinkTimer>, - time: Res<Time>, - active_editor: ResMut<Focus>, - mut cosmic_editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>, -) { - if let Some(e) = active_editor.0 { - timer.0.tick(time.delta()); - if !timer.0.just_finished() && !active_editor.is_changed() { - return; - } - visibility.0 = !visibility.0; - - // always start cursor visible on focus - if active_editor.is_changed() { - visibility.0 = true; - timer.0.set_elapsed(Duration::ZERO); - } - - let new_color = if visibility.0 { - None - } else { - Some(cosmic_text::Color::rgba(0, 0, 0, 0)) }; - if let Ok(mut editor) = cosmic_editor_q.get_mut(e) { - let editor = &mut editor.0; - let mut cursor = editor.cursor(); - cursor.color = new_color; - editor.set_cursor(cursor); - editor.buffer_mut().set_redraw(true); - } - } -} - -pub(crate) fn freeze_cursor_blink( - mut visibility: ResMut<CursorVisibility>, - mut timer: ResMut<CursorBlinkTimer>, - active_editor: Res<Focus>, - keys: Res<ButtonInput<KeyCode>>, - char_evr: EventReader<ReceivedCharacter>, - mut editor_q: Query<&mut CosmicEditor, Without<ReadOnly>>, -) { - let inputs = [ - KeyCode::ArrowLeft, - KeyCode::ArrowRight, - KeyCode::ArrowUp, - KeyCode::ArrowDown, - KeyCode::Backspace, - KeyCode::Enter, - ]; - if !keys.any_pressed(inputs) && char_evr.is_empty() { - return; - } - - if let Some(e) = active_editor.0 { - if let Ok(mut editor) = editor_q.get_mut(e) { - timer.0.set_elapsed(Duration::ZERO); - visibility.0 = true; - let mut cursor = editor.0.cursor(); - cursor.color = None; - editor.0.set_cursor(cursor); - editor.0.buffer_mut().set_redraw(true); - } - } -} - -pub(crate) fn hide_inactive_or_readonly_cursor( - mut cosmic_editor_q_readonly: Query<&mut CosmicEditor, With<ReadOnly>>, - mut cosmic_editor_q_editable: Query<(Entity, &mut CosmicEditor), Without<ReadOnly>>, - active_editor: Res<Focus>, -) { - for mut editor in &mut cosmic_editor_q_readonly.iter_mut() { - let mut cursor = editor.0.cursor(); - cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0)); - editor.0.set_cursor(cursor); - editor.0.buffer_mut().set_redraw(true); - } - - for (e, mut editor) in &mut cosmic_editor_q_editable.iter_mut() { - if active_editor.is_none() || e != active_editor.0.unwrap() { - let mut cursor = editor.0.cursor(); - if cursor.color == Some(cosmic_text::Color::rgba(0, 0, 0, 0)) { + // Draw glyphs + if let Some(mut editor) = editor { + if !editor.redraw() { continue; } - cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0)); - editor.0.set_cursor(cursor); - editor.0.buffer_mut().set_redraw(true); - } - } -} - -pub(crate) fn set_initial_scale( - window_q: Query<&Window, With<PrimaryWindow>>, - mut cosmic_query: Query<&mut CosmicMetrics, Added<CosmicMetrics>>, -) { - let scale = window_q.single().scale_factor(); - - for mut metrics in &mut cosmic_query.iter_mut() { - if metrics.scale_factor != DEFAULT_SCALE_PLACEHOLDER { - continue; - } - - metrics.scale_factor = scale; - } -} - -pub(crate) fn on_scale_factor_change( - mut scale_factor_changed: EventReader<WindowScaleFactorChanged>, - mut cosmic_query: Query<(&mut CosmicEditor, &CosmicMetrics, &mut XOffset)>, - mut font_system: ResMut<CosmicFontSystem>, -) { - if !scale_factor_changed.is_empty() { - let new_scale_factor = scale_factor_changed.read().last().unwrap().scale_factor as f32; - for (mut editor, metrics, mut x_offset) in &mut cosmic_query.iter_mut() { - let font_system = &mut font_system.0; - let metrics = - Metrics::new(metrics.font_size, metrics.line_height).scale(new_scale_factor); - editor.0.buffer_mut().set_metrics(font_system, metrics); - editor.0.buffer_mut().set_redraw(true); - - *x_offset = XOffset(None); - } - } -} + let cursor_opacity = if editor.cursor_visible && readonly_opt.is_none() { + (cursor_color.0.a() * 255.) as u8 + } else { + 0 + }; -pub(crate) fn swap_target_handle( - source_q: Query<&Handle<Image>, (Changed<Handle<Image>>, With<CosmicEditor>)>, - mut dest_q: Query< - ( - Option<&mut Handle<Image>>, - Option<&mut UiImage>, - &CosmicSource, - ), - Without<CosmicEditor>, - >, -) { - // TODO: do this once - for (dest_handle_opt, dest_ui_opt, source_entity) in dest_q.iter_mut() { - if let Ok(source_handle) = source_q.get(source_entity.0) { - if let Some(mut dest_handle) = dest_handle_opt { - *dest_handle = source_handle.clone_weak(); - } - if let Some(mut dest_ui) = dest_ui_opt { - dest_ui.texture = source_handle.clone_weak(); - } - } - } -} + let cursor_color = Color::rgba( + (cursor_color.r() * 255.) as u8, + (cursor_color.g() * 255.) as u8, + (cursor_color.b() * 255.) as u8, + cursor_opacity, + ); -pub(crate) fn hide_password_text( - mut editor_q: Query<(Entity, &mut CosmicEditor, &CosmicAttrs, &PasswordInput)>, - mut font_system: ResMut<CosmicFontSystem>, - mut password_input_states: ResMut<PasswordValues>, - active_editor: Res<Focus>, -) { - for (entity, mut cosmic_editor, attrs, password) in editor_q.iter_mut() { - let text = cosmic_editor.get_text(); - let select_opt = cosmic_editor.0.select_opt(); - let mut cursor = cosmic_editor.0.cursor(); + let selection_color = Color::rgba( + (selection_color.r() * 255.) as u8, + (selection_color.g() * 255.) as u8, + (selection_color.b() * 255.) as u8, + (selection_color.a() * 255.) as u8, + ); - if !text.is_empty() { - cosmic_editor.set_text( - CosmicText::OneStyle(format!("{}", password.0).repeat(text.chars().count())), - attrs.0.clone(), + editor.draw( &mut font_system.0, + &mut swash_cache_state.swash_cache, + font_color, + cursor_color, + selection_color, + draw_closure, ); - - // multiply cursor idx and select_opt end point by password char length - // the actual char length cos '鈼�' is 3x as long as 'a' - // This operation will need to be undone when resetting. - // - // Currently breaks entering multi-byte chars - - let char_len = password.0.len_utf8(); - - let select_opt = match select_opt { - Some(mut select) => { - select.index *= char_len; - Some(select) - } - None => None, - }; - - cursor.index *= char_len; - - cosmic_editor.0.set_select_opt(select_opt); - - // Fixes stuck cursor on password inputs - if let Some(active) = active_editor.0 { - if entity != active { - cursor.color = Some(cosmic_text::Color::rgba(0, 0, 0, 0)); - } - } - - cosmic_editor.0.set_cursor(cursor); - } - - let glyph_idx = match cosmic_editor.0.buffer().lines[0].layout_opt() { - Some(_) => cosmic_editor.0.buffer().layout_cursor(&cursor).glyph, - None => 0, - }; - - password_input_states.0.insert(entity, (text, glyph_idx)); - } -} - -pub(crate) fn restore_password_text( - mut editor_q: Query<(Entity, &mut CosmicEditor, &CosmicAttrs, &PasswordInput)>, - mut font_system: ResMut<CosmicFontSystem>, - password_input_states: Res<PasswordValues>, -) { - for (entity, mut cosmic_editor, attrs, password) in editor_q.iter_mut() { - if let Some((text, _glyph_idx)) = password_input_states.0.get(&entity) { - if !text.is_empty() { - let char_len = password.0.len_utf8(); - - let mut cursor = cosmic_editor.0.cursor(); - let select_opt = match cosmic_editor.0.select_opt() { - Some(mut select) => { - select.index /= char_len; - Some(select) - } - None => None, - }; - - cursor.index /= char_len; - - cosmic_editor.set_text( - crate::CosmicText::OneStyle(text.clone()), - attrs.0.clone(), - &mut font_system.0, - ); - - cosmic_editor.0.set_select_opt(select_opt); - cosmic_editor.0.set_cursor(cursor); + editor.set_redraw(false); + } else { + if !buffer.redraw() { + continue; } + buffer.draw( + &mut font_system.0, + &mut swash_cache_state.swash_cache, + font_color, + draw_closure, + ); + buffer.set_redraw(false); } - } -} - -pub(crate) fn show_placeholder( - mut editor_q: Query<( - Entity, - &mut CosmicEditor, - &PlaceholderText, - &PlaceholderAttrs, - )>, - mut font_system: ResMut<CosmicFontSystem>, - mut commands: Commands, -) { - for (entity, mut cosmic_editor, placeholder, attrs) in editor_q.iter_mut() { - if cosmic_editor.get_text().is_empty() { - cosmic_editor.set_text(placeholder.0.clone(), attrs.0.clone(), &mut font_system.0); - - let mut cursor = cosmic_editor.0.cursor(); - cursor.index = 0; - cosmic_editor.0.set_cursor(cursor); - commands.entity(entity).insert(Placeholder); - } else { - commands.entity(entity).remove::<Placeholder>(); + if let Some(prev_image) = images.get_mut(canvas) { + prev_image.data.clear(); + prev_image.data.extend_from_slice(pixels.as_slice()); + prev_image.resize(Extent3d { + width: size.0.x as u32, + height: size.0.y as u32, + depth_or_array_layers: 1, + }); } } } - -pub(crate) fn restore_placeholder_text( - mut editor_q: Query<(&mut CosmicEditor, &CosmicAttrs), With<Placeholder>>, - mut font_system: ResMut<CosmicFontSystem>, -) { - for (mut cosmic_editor, attrs) in editor_q.iter_mut() { - cosmic_editor.set_text( - CosmicText::OneStyle("".into()), - attrs.0.clone(), - &mut font_system.0, - ); - } -} diff --git a/util/Cargo.toml b/util/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..51de83365dfe6af657fa3258f3c808e5a1af1b7c --- /dev/null +++ b/util/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "util" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy = { version = "0.13", default-features = false, features = [ + "bevy_asset", + "bevy_core_pipeline", + "bevy_render", + "bevy_scene", + "bevy_sprite", + "bevy_text", + "bevy_ui", + "bevy_winit", + "png", + "x11", + "webgpu", +] } +bevy_cosmic_edit = { version = "*", path = "../" } diff --git a/util/src/lib.rs b/util/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..405152cd2ac7aaa293d14eb85d459cc88788238a --- /dev/null +++ b/util/src/lib.rs @@ -0,0 +1,84 @@ +// Common functions for examples +use bevy::{prelude::*, window::PrimaryWindow}; +use bevy_cosmic_edit::*; + +pub fn deselect_editor_on_esc(i: Res<ButtonInput<KeyCode>>, mut focus: ResMut<FocusedWidget>) { + if i.just_pressed(KeyCode::Escape) { + focus.0 = None; + } +} + +pub fn change_active_editor_sprite( + mut commands: Commands, + windows: Query<&Window, With<PrimaryWindow>>, + buttons: Res<ButtonInput<MouseButton>>, + mut cosmic_edit_query: Query< + (&mut Sprite, &GlobalTransform, &Visibility, Entity), + (With<CosmicBuffer>, Without<ReadOnly>), + >, + camera_q: Query<(&Camera, &GlobalTransform)>, +) { + let window = windows.single(); + let (camera, camera_transform) = camera_q.single(); + if buttons.just_pressed(MouseButton::Left) { + for (sprite, node_transform, visibility, entity) in &mut cosmic_edit_query.iter_mut() { + if visibility == Visibility::Hidden { + continue; + } + let size = sprite.custom_size.unwrap_or(Vec2::ONE); + let x_min = node_transform.affine().translation.x - size.x / 2.; + let y_min = node_transform.affine().translation.y - size.y / 2.; + let x_max = node_transform.affine().translation.x + size.x / 2.; + let y_max = node_transform.affine().translation.y + size.y / 2.; + if let Some(pos) = window.cursor_position() { + if let Some(pos) = camera.viewport_to_world_2d(camera_transform, pos) { + if x_min < pos.x && pos.x < x_max && y_min < pos.y && pos.y < y_max { + commands.insert_resource(FocusedWidget(Some(entity))) + }; + } + }; + } + } +} + +pub fn change_active_editor_ui( + mut commands: Commands, + mut interaction_query: Query< + (&Interaction, &CosmicSource), + (Changed<Interaction>, Without<ReadOnly>), + >, +) { + for (interaction, source) in interaction_query.iter_mut() { + if let Interaction::Pressed = interaction { + commands.insert_resource(FocusedWidget(Some(source.0))); + } + } +} + +pub fn print_editor_text( + text_inputs_q: Query<&CosmicEditor>, + mut previous_value: Local<Vec<String>>, +) { + for text_input in text_inputs_q.iter() { + let current_text: Vec<String> = text_input.with_buffer(|buf| { + buf.lines + .iter() + .map(|bl| bl.text().to_string()) + .collect::<Vec<_>>() + }); + if current_text == *previous_value { + return; + } + *previous_value = current_text.clone(); + info!("Widget text: {:?}", current_text); + } +} + +pub fn bevy_color_to_cosmic(color: bevy::prelude::Color) -> CosmicColor { + CosmicColor::rgba( + (color.r() * 255.) as u8, + (color.g() * 255.) as u8, + (color.b() * 255.) as u8, + (color.a() * 255.) as u8, + ) +}