Skip to content
Snippets Groups Projects
Unverified Commit aa836324 authored by sam edelsten's avatar sam edelsten Committed by GitHub
Browse files

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: default avatarStaffEngineer <111751109+StaffEngineer@users.noreply.github.com>
Co-authored-by: default avatarStaffEngineer <velo.app1@gmail.com>
parent c200e18d
No related branches found
No related tags found
No related merge requests found
# Changelog
## Version 0.18.0 (2024)
- Add text placeholder plugin
## Version 0.17.0 (2024)
- Update to cosmic-text 0.11.2
......
......@@ -406,7 +406,7 @@ dependencies = [
[[package]]
name = "bevy_cosmic_edit"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"arboard",
"bevy",
......
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/" }
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();
}
......@@ -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,
......
......@@ -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);
}
}
}
......
......@@ -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();
......
#[cfg(feature = "placeholder")]
pub mod placeholder;
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;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment