Skip to content
Snippets Groups Projects
placeholder.rs 6.49 KiB
Newer Older
Caleb Yates's avatar
Caleb Yates committed
use crate::{
    cosmic_edit::DefaultAttrs, input::CosmicTextChanged, input::InputSet, prelude::*,
Caleb Yates's avatar
Caleb Yates committed
    render::RenderSet,
};
use cosmic_text::{Attrs, Edit};

Caleb Yates's avatar
Caleb Yates committed
/// Component to be added to an entity with a [`CosmicEditBuffer`] add placeholder text
///
/// ```
/// # use bevy::prelude::*;
Caleb Yates's avatar
Caleb Yates committed
/// # use bevy_cosmic_edit::prelude::*;
/// # use bevy_cosmic_edit::cosmic_text::Attrs;
/// use bevy_cosmic_edit::placeholder::Placeholder;
Caleb Yates's avatar
Caleb Yates committed
///
/// # fn setup(mut commands: Commands) {
Caleb Yates's avatar
Caleb Yates committed
/// commands.spawn((
///     CosmicEditBuffer::default(),
///     Sprite {
///         custom_size: Some(Vec2::new(300.0, 40.0)),
///         ..default()
///     },
Caleb Yates's avatar
Caleb Yates committed
///     Placeholder::new("Email", Attrs::new().color(Color::from(bevy::color::palettes::css::GRAY).to_cosmic())),
/// ));
/// # }
/// # fn main() {
/// #     App::new()
/// #         .add_plugins(MinimalPlugins)
/// #         .add_plugins(CosmicEditPlugin::default())
/// #         .add_systems(Startup, setup);
/// # }
#[derive(Component)]
pub struct Placeholder {
    /// Placeholder text content
    pub text: &'static str,
    /// Text attributes for placeholder text
    pub attrs: Attrs<'static>,
    active: bool,
}

impl Placeholder {
    /// Create a new [`Placeholder`] component with given text and attributes
    pub fn new(text: impl Into<&'static str>, attrs: Attrs<'static>) -> Self {
        Self {
            active: false,
            text: text.into(),
            attrs,
        }
    }

    pub fn is_active(&self) -> bool {
        self.active
    }
sam edelsten's avatar
sam edelsten committed
pub(crate) fn plugin(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(InputSet)
            .before(RenderSet),
    );
}

fn add_placeholder_to_buffer(
    mut q: Query<(EditorBuffer, &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;
    }
}