From aa8363246894ad8606d1bf4e0142890c3ee01f67 Mon Sep 17 00:00:00 2001 From: sam edelsten <43527203+bytemunch@users.noreply.github.com> Date: Sat, 6 Apr 2024 13:32:57 +0100 Subject: [PATCH] internal placeholder plugin (#125) * internal placeholder plugin * fix panic on placeholder editor input * remove autofocus in placeholder example * make `Placeholder.active` private * remove placeholder on input * fix multi-byte char in placeholder * show placeholder on empty editor * add guards to placeholder add fns * fix placeholder displaying incorrectly on input * fix flash when backspacing empty placeholder also properly fix the first-char display error * hacky fix for delete key breaking placeholder * fix newline issues in placeholder * fix clippy * update changelog, bump version --------- Co-authored-by: StaffEngineer <111751109+StaffEngineer@users.noreply.github.com> Co-authored-by: StaffEngineer <velo.app1@gmail.com> --- CHANGELOG.md | 4 + Cargo.lock | 2 +- Cargo.toml | 11 +- examples/placeholder.rs | 80 +++++++++++++ src/buffer.rs | 2 - src/input.rs | 3 - src/lib.rs | 21 +++- src/plugins/mod.rs | 2 + src/plugins/placeholder/mod.rs | 199 +++++++++++++++++++++++++++++++++ 9 files changed, 314 insertions(+), 10 deletions(-) create mode 100644 examples/placeholder.rs create mode 100644 src/plugins/mod.rs create mode 100644 src/plugins/placeholder/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 608db70..f0f93e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.18.0 (2024) + +- Add text placeholder plugin + ## Version 0.17.0 (2024) - Update to cosmic-text 0.11.2 diff --git a/Cargo.lock b/Cargo.lock index 7622718..bcd40f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,7 +406,7 @@ dependencies = [ [[package]] name = "bevy_cosmic_edit" -version = "0.17.0" +version = "0.18.0" dependencies = [ "arboard", "bevy", diff --git a/Cargo.toml b/Cargo.toml index c47d624..7f44d8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ workspace = { members = ["util"] } [package] name = "bevy_cosmic_edit" -version = "0.17.0" +version = "0.18.0" edition = "2021" license = "MIT OR Apache-2.0" description = "Bevy cosmic-text multiline text input" @@ -13,6 +13,7 @@ exclude = ["assets/*"] [features] multicam = [] +placeholder = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -43,8 +44,12 @@ arboard = "3.2.0" js-sys = "0.3.67" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4.42" -web-sys = { version = "0.3.67", features = ["Clipboard", "Navigator", "Window"] } +web-sys = { version = "0.3.67", features = [ + "Clipboard", + "Navigator", + "Window", +] } [dev-dependencies] insta = "1.29.0" -util = {path="./util/"} +util = { path = "./util/" } diff --git a/examples/placeholder.rs b/examples/placeholder.rs new file mode 100644 index 0000000..bc081ea --- /dev/null +++ b/examples/placeholder.rs @@ -0,0 +1,80 @@ +use bevy::prelude::*; +use bevy_cosmic_edit::{placeholder::Placeholder, *}; +use util::{ + bevy_color_to_cosmic, change_active_editor_ui, deselect_editor_on_esc, print_editor_text, +}; + +fn setup(mut commands: Commands, mut font_system: ResMut<CosmicFontSystem>) { + let camera_bundle = Camera2dBundle { + camera: Camera { + clear_color: ClearColorConfig::Custom(Color::PINK), + ..default() + }, + ..default() + }; + commands.spawn(camera_bundle); + + let mut attrs = Attrs::new(); + attrs = attrs.family(Family::Name("Victor Mono")); + attrs = attrs.color(CosmicColor::rgb(0x94, 0x00, 0xD3)); + + let cosmic_edit = + commands + .spawn(( + CosmicEditBundle { + buffer: CosmicBuffer::new(&mut font_system, Metrics::new(20., 20.)) + .with_rich_text(&mut font_system, vec![("", attrs)], attrs), + text_position: CosmicTextPosition::Center, + ..default() + }, + Placeholder::new( + "Placeholder", + attrs.color(bevy_color_to_cosmic(Color::GRAY)), + ), + )) + .id(); + + commands + .spawn( + // Use buttonbundle for layout + // Includes Interaction and UiImage which are used by the plugin. + ButtonBundle { + style: Style { + width: Val::Percent(100.), + height: Val::Percent(100.), + ..default() + }, + ..default() + }, + ) + // point editor at this entity. + // Plugin looks for UiImage and sets it's + // texture to the editor's rendered image + .insert(CosmicSource(cosmic_edit)); +} + +fn main() { + let font_bytes: &[u8] = include_bytes!("../assets/fonts/VictorMono-Regular.ttf"); + let font_config = CosmicFontConfig { + fonts_dir_path: None, + font_bytes: Some(vec![font_bytes]), + load_system_fonts: true, + }; + + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CosmicEditPlugin { + font_config, + ..default() + }) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + print_editor_text, + change_active_editor_ui, + deselect_editor_on_esc, + ), + ) + .run(); +} diff --git a/src/buffer.rs b/src/buffer.rs index 8f58564..1d32295 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -15,8 +15,6 @@ impl<'s, 'r> CosmicBuffer { 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, diff --git a/src/input.rs b/src/input.rs index 5052197..087cef0 100644 --- a/src/input.rs +++ b/src/input.rs @@ -310,7 +310,6 @@ pub(crate) fn input_kb( } if should_jump && keys.just_pressed(KeyCode::Home) { editor.action(&mut font_system.0, Action::Motion(Motion::BufferStart)); - editor.set_redraw(true); if !shift { editor.set_selection(Selection::None); } @@ -318,7 +317,6 @@ pub(crate) fn input_kb( } if should_jump && keys.just_pressed(KeyCode::End) { editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd)); - editor.set_redraw(true); if !shift { editor.set_selection(Selection::None); } @@ -534,7 +532,6 @@ pub(crate) fn input_kb( for c in b { let c: char = (*c).into(); editor.action(&mut font_system.0, Action::Insert(c)); - editor.set_redraw(true); } } } diff --git a/src/lib.rs b/src/lib.rs index 0cc9cf3..be98184 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,9 +7,13 @@ mod input; mod layout; mod render; +mod plugins; +pub use plugins::*; + use std::{path::PathBuf, time::Duration}; use bevy::{prelude::*, transform::TransformSystem}; + use buffer::{ add_font_system, set_editor_redraw, set_initial_scale, set_redraw, swap_target_handle, }; @@ -192,6 +196,9 @@ impl Default for CosmicFontConfig { } } +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct KbInput; + /// Plugin struct that adds systems and initializes resources related to cosmic edit functionality. #[derive(Default)] pub struct CosmicEditPlugin { @@ -224,7 +231,10 @@ impl Plugin for CosmicEditPlugin { .chain(), ) .add_systems(PreUpdate, (input_mouse,).chain()) - .add_systems(Update, (input_kb, reshape, blink_cursor).chain()) + .add_systems( + Update, + (input_kb, reshape, blink_cursor).chain().in_set(KbInput), + ) .add_systems( PostUpdate, ( @@ -264,9 +274,18 @@ impl Plugin for CosmicEditPlugin { app.insert_resource(WasmPasteAsyncChannel { tx, rx }) .add_systems(Update, poll_wasm_paste); } + + add_feature_plugins(app); } } +fn add_feature_plugins(app: &mut App) -> &mut App { + #[cfg(feature = "placeholder")] + app.add_plugins(plugins::placeholder::PlaceholderPlugin); + + app +} + 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(); diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs new file mode 100644 index 0000000..2c07c75 --- /dev/null +++ b/src/plugins/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "placeholder")] +pub mod placeholder; diff --git a/src/plugins/placeholder/mod.rs b/src/plugins/placeholder/mod.rs new file mode 100644 index 0000000..65d7b70 --- /dev/null +++ b/src/plugins/placeholder/mod.rs @@ -0,0 +1,199 @@ +use bevy::prelude::*; +use cosmic_text::{Attrs, Edit}; + +use crate::{ + CosmicBuffer, CosmicEditor, CosmicFontSystem, CosmicTextChanged, DefaultAttrs, KbInput, +}; + +#[derive(Component)] +pub struct Placeholder { + pub text: &'static str, + pub attrs: Attrs<'static>, + active: bool, +} + +impl Placeholder { + pub fn new(text: impl Into<&'static str>, attrs: Attrs<'static>) -> Self { + Self { + active: false, + text: text.into(), + attrs, + } + } +} + +pub struct PlaceholderPlugin; + +impl Plugin for PlaceholderPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + add_placeholder_to_buffer, + add_placeholder_to_editor, + move_cursor_to_start_of_placeholder, + remove_placeholder_on_input, + ) + .chain() + .after(KbInput), + ); + } +} + +fn add_placeholder_to_buffer( + mut q: Query<(&mut CosmicBuffer, &mut Placeholder)>, + mut font_system: ResMut<CosmicFontSystem>, +) { + for (mut buffer, mut placeholder) in q.iter_mut() { + if placeholder.active { + return; + } + + if buffer.get_text().is_empty() { + buffer.set_text(&mut font_system, placeholder.text, placeholder.attrs); + placeholder.active = true; + } + } +} + +fn add_placeholder_to_editor( + mut q: Query<(&mut CosmicEditor, &mut Placeholder)>, + mut font_system: ResMut<CosmicFontSystem>, +) { + for (mut editor, mut placeholder) in q.iter_mut() { + if placeholder.active { + // PERF: Removing this guard fixes single char placeholder deletion + // BUT makes the check and buffer update run every frame + // return; + } + + editor.with_buffer_mut(|buffer| { + if buffer.lines.len() > 1 { + return; + } + + if buffer.lines[0].clone().into_text().is_empty() { + buffer.set_text( + &mut font_system, + placeholder.text, + placeholder.attrs, + cosmic_text::Shaping::Advanced, + ); + placeholder.active = true; + buffer.set_redraw(true); + } + }) + } +} + +fn move_cursor_to_start_of_placeholder(mut q: Query<(&mut CosmicEditor, &mut Placeholder)>) { + for (mut editor, placeholder) in q.iter_mut() { + if placeholder.active { + editor.set_cursor(cosmic_text::Cursor::new(0, 0)); + } + } +} + +fn remove_placeholder_on_input( + mut q: Query<(&mut CosmicEditor, &mut Placeholder, &DefaultAttrs)>, + evr: EventReader<CosmicTextChanged>, + mut font_system: ResMut<CosmicFontSystem>, +) { + for (mut editor, mut placeholder, attrs) in q.iter_mut() { + if !placeholder.active { + return; + } + if evr.is_empty() { + return; + } + + let mut lines = 0; + + let last_line = editor.with_buffer_mut(|b| { + lines = b.lines.len(); + + if lines > 1 { + let mut full_text: String = b + .lines + .iter() + .map(|l| { + let mut s = l.clone().into_text().replace(placeholder.text, ""); + // Extra newline on enter to prevent reading as an empty buffer + s.push('\n'); + s + }) + .collect(); + + if lines > 2 { + // for pasted text, remove trailing newline + full_text = full_text + .strip_suffix('\n') + .expect("oop something broke in multiline placeholder removal") + .to_string(); + } + + b.set_text( + &mut font_system, + full_text.as_str(), + attrs.0.as_attrs(), + cosmic_text::Shaping::Advanced, + ); + + let last_line = full_text.lines().last(); + + return last_line.map(|line| line.to_string()); + } + + let single_line = b.lines[0].clone().into_text().replace(placeholder.text, ""); + + if single_line.is_empty() { + return None; + } + + { + // begin hacky fix for delete key in empty placeholder widget + + let p = placeholder + .text + .chars() + .next() + .expect("Couldn't get first char of placeholder."); + + let laceholder = placeholder + .text + .strip_prefix(p) + .expect("Couldn't remove first char of placeholder."); + + if single_line.as_str() == laceholder { + b.set_text( + &mut font_system, + placeholder.text, + placeholder.attrs, + cosmic_text::Shaping::Advanced, + ); + return None; + } + } // end hacky fix + + b.set_text( + &mut font_system, + single_line.as_str(), + attrs.0.as_attrs(), + cosmic_text::Shaping::Advanced, + ); + + Some(single_line) + }); + + let Some(last_line) = last_line else { + return; + }; + + editor.set_cursor(cosmic_text::Cursor::new( + (lines - 1).max(0), + last_line.bytes().len(), + )); + + placeholder.active = false; + } +} -- GitLab