amethyst_ui 0.15.3

Amethyst UI crate
//! Module holding the components related to text and text editing.

use crate::Anchor;

use derivative::Derivative;
use serde::{Deserialize, Serialize};
use unicode_normalization::{char::is_combining_mark, UnicodeNormalization};
use winit::{ElementState, Event, MouseButton, WindowEvent};

use amethyst_core::{
    ecs::prelude::{
        Component, DenseVecStorage, Join, Read, ReadExpect, ReadStorage, System, SystemData,
        WriteStorage,
    },
    shrev::{EventChannel, ReaderId},
    timing::Time,
};
use amethyst_derive::SystemDesc;
use amethyst_window::ScreenDimensions;

use super::*;

/// How lines should behave when they are longer than the maximum line length.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
pub enum LineMode {
    /// Single line. It ignores line breaks.
    Single,
    /// Multiple lines. The text will automatically wrap when exceeding the maximum width.
    Wrap,
}

/// A component used to display text in this entity's UiTransform
#[derive(Clone, Derivative, Serialize)]
#[derivative(Debug)]
pub struct UiText {
    /// The string rendered by this.
    pub text: String,
    /// The height of a line of text in pixels.
    pub font_size: f32,
    /// The color of the rendered text, using a range of 0.0 to 1.0 per channel.
    pub color: [f32; 4],
    /// The font used for rendering.
    #[serde(skip)]
    pub font: FontHandle,
    /// If true this will be rendered as dots instead of the text.
    pub password: bool,
    /// How the text should handle new lines.
    pub line_mode: LineMode,
    /// How to align the text within its `UiTransform`.
    pub align: Anchor,
    /// Cached glyph positions including invisible characters, used to process mouse highlighting.
    #[serde(skip)]
    pub(crate) cached_glyphs: Vec<CachedGlyph>,
}

#[derive(Clone, Copy, Debug)]
pub(crate) struct CachedGlyph {
    pub(crate) x: f32,
    pub(crate) y: f32,
    pub(crate) advance_width: f32,
}

impl UiText {
    /// Initializes a new UiText
    ///
    /// # Parameters
    ///
    /// * `font`: A handle to a `Font` asset
    /// * `text`: The glyphs to render
    /// * `color`: RGBA color with a maximum of 1.0 and a minimum of 0.0 for each channel
    /// * `font_size`: A uniform scale applied to the glyphs
    /// * `line_mode`: Text mode allowing single line or multiple lines
    /// * `align`: Text alignment within its `UiTransform`
    pub fn new(
        font: FontHandle,
        text: String,
        color: [f32; 4],
        font_size: f32,
        line_mode: LineMode,
        align: Anchor,
    ) -> UiText {
        UiText {
            text,
            color,
            font_size,
            font,
            password: false,
            line_mode,
            align,
            cached_glyphs: Vec::new(),
        }
    }
}

impl Component for UiText {
    type Storage = DenseVecStorage<Self>;
}

/// If this component is attached to an entity with a UiText then that UiText is editable.
/// This component also controls how that editing works.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TextEditing {
    /// The current editing cursor position, specified in terms of glyphs, not characters.
    pub cursor_position: isize,
    /// The maximum graphemes permitted in this string.
    pub max_length: usize,
    /// The amount and direction of glyphs highlighted relative to the cursor.
    pub highlight_vector: isize,
    /// The color of the text itself when highlighted.
    pub selected_text_color: [f32; 4],
    /// The text background color when highlighted.
    pub selected_background_color: [f32; 4],
    /// If this is true the text will use a block cursor for editing.  Otherwise this uses a
    /// standard line cursor.  This is not recommended if your font is not monospace.
    pub use_block_cursor: bool,

    /// This value is used to control cursor blinking.
    ///
    /// When it is greater than 0.5 / CURSOR_BLINK_RATE the cursor should not display, when it
    /// is greater than or equal to 1.0 / CURSOR_BLINK_RATE it should be reset to 0.  When the
    /// player types it should be reset to 0.
    pub(crate) cursor_blink_timer: f32,
}

impl TextEditing {
    /// Create a new TextEditing Component
    pub fn new(
        max_length: usize,
        selected_text_color: [f32; 4],
        selected_background_color: [f32; 4],
        use_block_cursor: bool,
    ) -> TextEditing {
        TextEditing {
            cursor_position: 0,
            max_length,
            highlight_vector: 0,
            selected_text_color,
            selected_background_color,
            use_block_cursor,
            cursor_blink_timer: 0.0,
        }
    }
}

impl Component for TextEditing {
    type Storage = DenseVecStorage<Self>;
}

/// This system processes the underlying UI data as needed.
#[derive(Debug, SystemDesc)]
#[system_desc(name(TextEditingMouseSystemDesc))]
pub struct TextEditingMouseSystem {
    /// A reader for winit events.
    #[system_desc(event_channel_reader)]
    reader: ReaderId<Event>,
    /// This is set to true while the left mouse button is pressed.
    #[system_desc(skip)]
    left_mouse_button_pressed: bool,
    /// The screen coordinates of the mouse
    #[system_desc(skip)]
    mouse_position: (f32, f32),
}

impl TextEditingMouseSystem {
    /// Creates a new instance of this system
    pub fn new(reader: ReaderId<Event>) -> Self {
        Self {
            reader,
            left_mouse_button_pressed: false,
            mouse_position: (0., 0.),
        }
    }
}

impl<'a> System<'a> for TextEditingMouseSystem {
    type SystemData = (
        WriteStorage<'a, UiText>,
        WriteStorage<'a, TextEditing>,
        ReadStorage<'a, Selected>,
        Read<'a, EventChannel<Event>>,
        ReadExpect<'a, ScreenDimensions>,
        Read<'a, Time>,
    );

    fn run(
        &mut self,
        (mut texts, mut text_editings, selecteds, events, screen_dimensions, time): Self::SystemData,
    ) {
        // Normalize text to ensure we can properly count the characters.
        // TODO: Possible improvement to be made if this can be moved only when inserting characters into ui text.
        for text in (&mut texts).join() {
            if (*text.text).chars().any(is_combining_mark) {
                let normalized = text.text.nfd().collect::<String>();
                text.text = normalized;
            }
        }

        // TODO: Finish TextEditingCursorSystem and remove this
        {
            for (text_editing, _) in (&mut text_editings, &selecteds).join() {
                text_editing.cursor_blink_timer += time.delta_real_seconds();
                if text_editing.cursor_blink_timer >= 0.5 {
                    text_editing.cursor_blink_timer = 0.0;
                }
            }
        }

        let mut just_pressed = false;
        let mut moved_while_pressed = false;

        // Process only if an editable text is selected.
        for event in events.read(&mut self.reader) {
            // Process events for the whole UI.
            match *event {
                Event::WindowEvent {
                    event: WindowEvent::CursorMoved { position, .. },
                    ..
                } => {
                    let hidpi = screen_dimensions.hidpi_factor() as f32;
                    self.mouse_position = (
                        position.x as f32 * hidpi,
                        (screen_dimensions.height() - position.y as f32) * hidpi,
                    );
                    if self.left_mouse_button_pressed {
                        moved_while_pressed = true;
                    }
                }
                Event::WindowEvent {
                    event:
                        WindowEvent::MouseInput {
                            button: MouseButton::Left,
                            state,
                            ..
                        },
                    ..
                } => match state {
                    ElementState::Pressed => {
                        just_pressed = true;
                        self.left_mouse_button_pressed = true;
                    }
                    ElementState::Released => {
                        self.left_mouse_button_pressed = false;
                    }
                },
                _ => {}
            }
        }

        for (ref mut text, ref mut text_editing, selected) in
            (&mut texts, &mut text_editings, selecteds.maybe()).join()
        {
            if selected.is_none() {
                // If an editable text field is no longer selected, we should reset
                // the highlight vector.
                text_editing.highlight_vector = 0;
            } else if just_pressed {
                // If we focused an editable text field be sure to position the cursor
                // in it.
                let (mouse_x, mouse_y) = self.mouse_position;
                text_editing.highlight_vector = 0;
                text_editing.cursor_position =
                    closest_glyph_index_to_mouse(mouse_x, mouse_y, &text.cached_glyphs);
                text_editing.cursor_blink_timer = 0.0;

                // The end of the text, while not a glyph, is still something
                // you'll likely want to click your cursor to, so if the cursor is
                // near the end of the text, check if we should put it at the end
                // of the text.
                if should_advance_to_end(mouse_x, text_editing, text) {
                    text_editing.cursor_position += 1;
                }
            } else if moved_while_pressed {
                let (mouse_x, mouse_y) = self.mouse_position;
                text_editing.highlight_vector =
                    closest_glyph_index_to_mouse(mouse_x, mouse_y, &text.cached_glyphs)
                        - text_editing.cursor_position;
                // The end of the text, while not a glyph, is still something
                // you'll likely want to click your cursor to, so if the cursor is
                // near the end of the text, check if we should put it at the end
                // of the text.
                if should_advance_to_end(mouse_x, text_editing, text) {
                    text_editing.highlight_vector += 1;
                }
            }
        }
    }
}

fn should_advance_to_end(mouse_x: f32, text_editing: &mut TextEditing, text: &mut UiText) -> bool {
    let cursor_pos = text_editing.cursor_position + text_editing.highlight_vector;
    let len = text.cached_glyphs.len() as isize;
    if cursor_pos + 1 == len {
        if let Some(last_glyph) = text.cached_glyphs.last() {
            if mouse_x - last_glyph.x > last_glyph.advance_width / 2.0 {
                return true;
            }
        }
    }

    false
}

fn closest_glyph_index_to_mouse(mouse_x: f32, mouse_y: f32, glyphs: &[CachedGlyph]) -> isize {
    glyphs
        .iter()
        .enumerate()
        .min_by(|(_, g1), (_, g2)| {
            let dist = |g: &CachedGlyph| {
                let dx = g.x - mouse_x;
                let dy = g.y - mouse_y;
                dx * dx + dy * dy
            };
            dist(g1).partial_cmp(&dist(g2)).expect("Unexpected NaN!")
        })
        .map(|(i, _)| i)
        .unwrap_or(0) as isize
}