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, World,
WriteStorage,
},
shrev::{EventChannel, ReaderId},
timing::Time,
SystemDesc,
};
use amethyst_derive::SystemDesc;
use amethyst_window::ScreenDimensions;
use super::*;
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
pub enum LineMode {
Single,
Wrap,
}
#[derive(Clone, Derivative, Serialize)]
#[derivative(Debug)]
pub struct UiText {
pub text: String,
pub font_size: f32,
pub color: [f32; 4],
#[serde(skip)]
pub font: FontHandle,
pub password: bool,
pub line_mode: LineMode,
pub align: Anchor,
#[serde(skip)]
pub(crate) cached_glyphs: Vec<CachedGlyph>,
}
#[derive(Debug, Clone)]
pub(crate) struct CachedGlyph {
pub(crate) x: f32,
pub(crate) y: f32,
pub(crate) advance_width: f32,
}
impl UiText {
pub fn new(font: FontHandle, text: String, color: [f32; 4], font_size: f32) -> UiText {
UiText {
text,
color,
font_size,
font: font.clone(),
password: false,
line_mode: LineMode::Single,
align: Anchor::Middle,
cached_glyphs: Vec::new(),
}
}
}
impl Component for UiText {
type Storage = DenseVecStorage<Self>;
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TextEditing {
pub cursor_position: isize,
pub max_length: usize,
pub highlight_vector: isize,
pub selected_text_color: [f32; 4],
pub selected_background_color: [f32; 4],
pub use_block_cursor: bool,
pub(crate) cursor_blink_timer: f32,
}
impl TextEditing {
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>;
}
#[derive(Debug, SystemDesc)]
#[system_desc(name(TextEditingMouseSystemDesc))]
pub struct TextEditingMouseSystem {
#[system_desc(event_channel_reader)]
reader: ReaderId<Event>,
#[system_desc(skip)]
left_mouse_button_pressed: bool,
#[system_desc(skip)]
mouse_position: (f32, f32),
}
impl TextEditingMouseSystem {
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,
) {
for text in (&mut texts).join() {
if (*text.text).chars().any(is_combining_mark) {
let normalized = text.text.nfd().collect::<String>();
text.text = normalized;
}
}
{
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;
}
}
}
for event in events.read(&mut self.reader) {
for (ref mut text, ref mut text_editing, _) in
(&mut texts, &mut text_editings, &selecteds).join()
{
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 {
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;
if should_advance_to_end(mouse_x, text_editing, text) {
text_editing.highlight_vector += 1;
}
}
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
button: MouseButton::Left,
state,
..
},
..
} => {
match state {
ElementState::Pressed => {
self.left_mouse_button_pressed = true;
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,
);
if should_advance_to_end(mouse_x, text_editing, text) {
text_editing.cursor_position += 1;
}
}
ElementState::Released => {
self.left_mouse_button_pressed = false;
}
}
}
_ => {}
}
}
}
}
}
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
}