Skip to content
Snippets Groups Projects
Commit 0b25cb59 authored by StarToaster's avatar StarToaster
Browse files

Added cursor for text boxes.

parent 98b4de46
No related branches found
No related tags found
No related merge requests found
...@@ -10,7 +10,7 @@ bevy_renderer = ["bevy"] ...@@ -10,7 +10,7 @@ bevy_renderer = ["bevy"]
[dependencies] [dependencies]
anyhow = { version = "1.0" } anyhow = { version = "1.0" }
nanoserde = "0.1.30" nanoserde = "0.1.30"
unicode-segmentation = "1.9" unicode-segmentation = "1.10.0"
# Provides UAX #14 line break segmentation # Provides UAX #14 line break segmentation
xi-unicode = "0.3" xi-unicode = "0.3"
......
...@@ -92,6 +92,11 @@ impl KayakFont { ...@@ -92,6 +92,11 @@ impl KayakFont {
width width
} }
/// Splits up the provided &str into grapheme clusters.
pub fn get_graphemes<'a>(&'a self, content: &'a str) -> Vec<&'a str> {
UnicodeSegmentation::graphemes(content, true).collect::<Vec<_>>()
}
/// Measures the given text content and calculates an appropriate layout /// Measures the given text content and calculates an appropriate layout
/// given a set of properties. /// given a set of properties.
/// ///
......
...@@ -86,7 +86,8 @@ pub struct KayakWidgets; ...@@ -86,7 +86,8 @@ pub struct KayakWidgets;
impl Plugin for KayakWidgets { impl Plugin for KayakWidgets {
fn build(&self, app: &mut bevy::prelude::App) { fn build(&self, app: &mut bevy::prelude::App) {
app.add_startup_system_to_stage(StartupStage::PostStartup, add_widget_systems); app.add_startup_system_to_stage(StartupStage::PostStartup, add_widget_systems)
.add_system(text_box::cursor_animation_system);
} }
} }
......
use bevy::prelude::{Bundle, Color, Commands, Component, Entity, In, Query}; use std::time::Instant;
use kayak_ui_macros::rsx;
use bevy::prelude::*;
use kayak_font::{KayakFont, TextProperties};
use kayak_ui_macros::{constructor, rsx};
use crate::{ use crate::{
context::WidgetName, context::WidgetName,
...@@ -8,6 +11,7 @@ use crate::{ ...@@ -8,6 +11,7 @@ use crate::{
on_event::OnEvent, on_event::OnEvent,
on_layout::OnLayout, on_layout::OnLayout,
prelude::{KChildren, KayakWidgetContext, OnChange}, prelude::{KChildren, KayakWidgetContext, OnChange},
render::font::FontMapping,
styles::{Edge, KStyle, RenderCommand, StyleProp, Units}, styles::{Edge, KStyle, RenderCommand, StyleProp, Units},
widget::Widget, widget::Widget,
widget_state::WidgetState, widget_state::WidgetState,
...@@ -15,7 +19,7 @@ use crate::{ ...@@ -15,7 +19,7 @@ use crate::{
text::{TextProps, TextWidgetBundle}, text::{TextProps, TextWidgetBundle},
BackgroundBundle, ClipBundle, BackgroundBundle, ClipBundle,
}, },
Focusable, Focusable, DEFAULT_FONT,
}; };
/// Props used by the [`TextBox`] widget /// Props used by the [`TextBox`] widget
...@@ -32,9 +36,27 @@ pub struct TextBoxProps { ...@@ -32,9 +36,27 @@ pub struct TextBoxProps {
pub value: String, pub value: String,
} }
#[derive(Component, Default, Clone, PartialEq)] #[derive(Component, Clone, PartialEq)]
pub struct TextBoxState { pub struct TextBoxState {
pub focused: bool, pub focused: bool,
pub graphemes: Vec<String>,
pub cursor_x: f32,
pub cursor_position: usize,
pub cursor_visible: bool,
pub cursor_last_update: Instant,
}
impl Default for TextBoxState {
fn default() -> Self {
Self {
focused: Default::default(),
graphemes: Default::default(),
cursor_x: 0.0,
cursor_position: Default::default(),
cursor_visible: Default::default(),
cursor_last_update: Instant::now(),
}
}
} }
pub struct TextBoxValue(pub String); pub struct TextBoxValue(pub String);
...@@ -80,7 +102,9 @@ pub fn text_box_render( ...@@ -80,7 +102,9 @@ pub fn text_box_render(
let state_entity = widget_context.use_state::<TextBoxState>( let state_entity = widget_context.use_state::<TextBoxState>(
&mut commands, &mut commands,
entity, entity,
TextBoxState::default(), TextBoxState {
..TextBoxState::default()
},
); );
if let Ok(state) = state_query.get(state_entity) { if let Ok(state) = state_query.get(state_entity) {
...@@ -119,6 +143,8 @@ pub fn text_box_render( ...@@ -119,6 +143,8 @@ pub fn text_box_render(
let current_value = text_box.value.clone(); let current_value = text_box.value.clone();
let cloned_on_change = on_change.clone(); let cloned_on_change = on_change.clone();
let style_font = styles.font.clone();
*on_event = OnEvent::new( *on_event = OnEvent::new(
move |In((event_dispatcher_context, _, mut event, _entity)): In<( move |In((event_dispatcher_context, _, mut event, _entity)): In<(
EventDispatcherContext, EventDispatcherContext,
...@@ -126,8 +152,38 @@ pub fn text_box_render( ...@@ -126,8 +152,38 @@ pub fn text_box_render(
Event, Event,
Entity, Entity,
)>, )>,
font_assets: Res<Assets<KayakFont>>,
font_mapping: Res<FontMapping>,
mut state_query: Query<&mut TextBoxState>| { mut state_query: Query<&mut TextBoxState>| {
match event.event_type { match event.event_type {
EventType::KeyDown(key_event) => {
if key_event.key() == KeyCode::Right {
if let Ok(mut state) = state_query.get_mut(state_entity) {
if state.cursor_position < state.graphemes.len() {
state.cursor_position += 1;
}
set_new_cursor_position(
&mut state,
&font_assets,
&font_mapping,
&style_font,
);
}
}
if key_event.key() == KeyCode::Left {
if let Ok(mut state) = state_query.get_mut(state_entity) {
if state.cursor_position > 0 {
state.cursor_position -= 1;
}
set_new_cursor_position(
&mut state,
&font_assets,
&font_mapping,
&style_font,
);
}
}
}
EventType::CharInput { c } => { EventType::CharInput { c } => {
let mut current_value = current_value.clone(); let mut current_value = current_value.clone();
let cloned_on_change = cloned_on_change.clone(); let cloned_on_change = cloned_on_change.clone();
...@@ -140,17 +196,61 @@ pub fn text_box_render( ...@@ -140,17 +196,61 @@ pub fn text_box_render(
} }
if is_backspace(c) { if is_backspace(c) {
if !current_value.is_empty() { if !current_value.is_empty() {
current_value.truncate(current_value.len() - 1); if let Ok(mut state) = state_query.get_mut(state_entity) {
// TODO: This doesn't respect graphemes!
current_value.remove(state.cursor_position - 1);
state.cursor_position -= 1;
}
} }
} else if !c.is_control() { } else if !c.is_control() {
current_value.push(c); if let Ok(mut state) = state_query.get_mut(state_entity) {
// TODO: This doesn't respect graphemes!
current_value.insert(state.cursor_position, c);
state.cursor_position += 1;
}
}
if let Ok(mut state) = state_query.get_mut(state_entity) {
// Update graphemes
set_graphemes(
&mut state,
&font_assets,
&font_mapping,
&style_font,
&current_value,
);
set_new_cursor_position(
&mut state,
&font_assets,
&font_mapping,
&style_font,
);
} }
cloned_on_change.set_value(current_value); cloned_on_change.set_value(current_value);
event.add_system(cloned_on_change); event.add_system(cloned_on_change);
} }
EventType::Focus => { EventType::Focus => {
if let Ok(mut state) = state_query.get_mut(state_entity) { if let Ok(mut state) = state_query.get_mut(state_entity) {
state.focused = true; state.focused = true;
// Update graphemes
set_graphemes(
&mut state,
&font_assets,
&font_mapping,
&style_font,
&current_value,
);
state.cursor_position = state.graphemes.len();
set_new_cursor_position(
&mut state,
&font_assets,
&font_mapping,
&style_font,
);
} }
} }
EventType::Blur => { EventType::Blur => {
...@@ -164,6 +264,16 @@ pub fn text_box_render( ...@@ -164,6 +264,16 @@ pub fn text_box_render(
}, },
); );
let cursor_styles = KStyle {
background_color: Color::rgba(0.933, 0.745, 0.745, 1.0).into(),
position_type: crate::styles::KPositionType::SelfDirected.into(),
top: Units::Pixels(5.0).into(),
left: Units::Pixels(state.cursor_x).into(),
width: Units::Pixels(2.0).into(),
height: Units::Pixels(26.0 - 10.0).into(),
..Default::default()
};
let parent_id = Some(entity); let parent_id = Some(entity);
rsx! { rsx! {
<BackgroundBundle styles={background_styles}> <BackgroundBundle styles={background_styles}>
...@@ -188,6 +298,13 @@ pub fn text_box_render( ...@@ -188,6 +298,13 @@ pub fn text_box_render(
..Default::default() ..Default::default()
}} }}
/> />
{
if state.focused && state.cursor_visible {
constructor! {
<BackgroundBundle styles={cursor_styles} />
}
}
}
</ClipBundle> </ClipBundle>
</BackgroundBundle> </BackgroundBundle>
} }
...@@ -203,3 +320,71 @@ pub fn text_box_render( ...@@ -203,3 +320,71 @@ pub fn text_box_render(
fn is_backspace(c: char) -> bool { fn is_backspace(c: char) -> bool {
c == '\u{8}' || c == '\u{7f}' c == '\u{8}' || c == '\u{7f}'
} }
fn set_graphemes(
state: &mut TextBoxState,
font_assets: &Res<Assets<KayakFont>>,
font_mapping: &FontMapping,
style_font: &StyleProp<String>,
current_value: &String,
) {
let font_handle = match style_font {
StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
_ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
};
if let Some(font) = font_assets.get(&font_handle) {
state.graphemes = font
.get_graphemes(&current_value)
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
}
}
fn set_new_cursor_position(
state: &mut TextBoxState,
font_assets: &Res<Assets<KayakFont>>,
font_mapping: &FontMapping,
style_font: &StyleProp<String>,
) {
let font_handle = match style_font {
StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
_ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
};
if let Some(font) = font_assets.get(&font_handle) {
let string_to_cursor = state.graphemes[0..state.cursor_position].join("");
let measurement = font.measure(
&string_to_cursor,
TextProperties {
font_size: 14.0,
line_height: 18.0,
max_size: (10000.0, 18.0),
alignment: kayak_font::Alignment::Start,
tab_size: 4,
},
);
state.cursor_x = measurement.size().0;
}
}
pub fn cursor_animation_system(
mut state_query: ParamSet<(Query<(Entity, &TextBoxState)>, Query<&mut TextBoxState>)>,
) {
let mut should_update = Vec::new();
for (entity, state) in state_query.p0().iter() {
if state.cursor_last_update.elapsed().as_secs_f32() > 0.5 && state.focused {
should_update.push(entity);
}
}
for state_entity in should_update.drain(..) {
if let Ok(mut state) = state_query.p1().get_mut(state_entity) {
state.cursor_last_update = Instant::now();
state.cursor_visible = !state.cursor_visible;
}
}
}
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