Skip to content
Snippets Groups Projects
  • sam edelsten's avatar
    internal placeholder plugin (#125) · aa836324
    sam edelsten authored
    
    * 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>
    Unverified
    aa836324
mod.rs 5.59 KiB
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;
    }
}