Newer
Older
use bevy::prelude::*;
use kayak_font::{KayakFont, TextProperties};
use kayak_ui_macros::{constructor, rsx};
event::{Event, EventType},
event_dispatcher::EventDispatcherContext,
on_event::OnEvent,
on_layout::OnLayout,
prelude::{KChildren, KayakWidgetContext, OnChange},
styles::{ComputedStyles, Edge, KPositionType, KStyle, RenderCommand, StyleProp, Units},
widgets::{
text::{TextProps, TextWidgetBundle},
BackgroundBundle, ClipBundle,
#[derive(Component, PartialEq, Eq, Default, Debug, Clone)]
/// If true, prevents the widget from being focused (and consequently edited)
/// The text to display when the user input is empty
/// The user input
///
/// This is a controlled state. You _must_ set this to the value to you wish to be displayed.
/// You can use the [`on_change`] callback to update this prop as the user types.
pub value: String,
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);
impl Widget for TextBoxProps {}

StarArawn
committed
/// A text box allows users to input text.
/// This text box is fairly simple and only supports basic input.
#[derive(Bundle)]
pub struct TextBoxBundle {
pub text_box: TextBoxProps,
pub styles: KStyle,
pub computed_styles: ComputedStyles,
pub on_event: OnEvent,
pub on_layout: OnLayout,
pub on_change: OnChange,
pub focusable: Focusable,
pub widget_name: WidgetName,
}
impl Default for TextBoxBundle {
fn default() -> Self {
Self {
text_box: Default::default(),
styles: Default::default(),
computed_styles: ComputedStyles::default(),
on_event: Default::default(),
on_layout: Default::default(),
on_change: Default::default(),
focusable: Default::default(),
widget_name: TextBoxProps::default().get_name(),
}
}
}
In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
mut query: Query<(
&KStyle,
&mut ComputedStyles,
&TextBoxProps,
&mut OnEvent,
&OnChange,
)>,
mut state_query: ParamSet<(Query<&TextBoxState>, Query<&mut TextBoxState>)>,
font_assets: Res<Assets<KayakFont>>,
font_mapping: Res<FontMapping>,
if let Ok((styles, mut computed_styles, text_box, mut on_event, on_change)) =
query.get_mut(entity)
{
let state_entity = widget_context.use_state::<TextBoxState>(
&mut commands,
entity,
let mut is_different = false;
if let Ok(state) = state_query.p0().get(state_entity) {
if state.current_value != text_box.value {
is_different = true;
}
}
let style_font = styles.font.clone();
if is_different {
if let Ok(mut state) = state_query.p1().get_mut(state_entity) {
state.current_value = text_box.value.clone();
// Update graphemes
set_graphemes(&mut state, &font_assets, &font_mapping, &style_font);
state.cursor_position = state.graphemes.len();
set_new_cursor_position(&mut state, &font_assets, &font_mapping, &style_font);
}
}
if let Ok(state) = state_query.p0().get(state_entity) {
*computed_styles = KStyle::default()
// Required styles
.with_style(KStyle {
render_command: RenderCommand::Layout.into(),
..Default::default()
})
// Apply any prop-given styles
// If not set by props, apply these styles
.with_style(KStyle {
top: Units::Pixels(0.0).into(),
bottom: Units::Pixels(0.0).into(),
height: Units::Pixels(26.0).into(),
// cursor: CursorIcon::Text.into(),
..Default::default()
let background_styles = KStyle {
render_command: StyleProp::Value(RenderCommand::Quad),
background_color: Color::rgba(0.160, 0.172, 0.235, 1.0).into(),
border_color: if state.focused {
Color::rgba(0.933, 0.745, 0.745, 1.0).into()
} else {
Color::rgba(0.360, 0.380, 0.474, 1.0).into()
},
border: Edge::new(0.0, 0.0, 0.0, 2.0).into(),
padding_left: Units::Pixels(5.0).into(),
padding_right: Units::Pixels(5.0).into(),
let cloned_on_change = on_change.clone();
*on_event = OnEvent::new(
move |In((event_dispatcher_context, _, mut event, _entity)): In<(
EventDispatcherContext,
WidgetState,
Event,
Entity,
)>,
font_assets: Res<Assets<KayakFont>>,
font_mapping: Res<FontMapping>,
mut state_query: Query<&mut TextBoxState>| {
match event.event_type {
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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 } => {
if let Ok(mut state) = state_query.get_mut(state_entity) {
let cloned_on_change = cloned_on_change.clone();
if !state.focused {
return (event_dispatcher_context, event);
}
let cursor_pos = state.cursor_position;
if is_backspace(c) {
if !state.current_value.is_empty() {
let char_pos: usize = state.graphemes[0..cursor_pos - 1]
.iter()
.map(|g| g.len())
.sum();
state.current_value.remove(char_pos);
let char_pos: usize = state.graphemes[0..cursor_pos]
.iter()
.map(|g| g.len())
.sum();
state.current_value.insert(char_pos, c);
state.cursor_position += 1;
}
// Update graphemes
set_graphemes(&mut state, &font_assets, &font_mapping, &style_font);
set_new_cursor_position(
&mut state,
&font_assets,
&font_mapping,
&style_font,
);
cloned_on_change.set_value(state.current_value.clone());
event.add_system(cloned_on_change);
EventType::Focus => {
if let Ok(mut state) = state_query.get_mut(state_entity) {
state.focused = true;
set_graphemes(&mut state, &font_assets, &font_mapping, &style_font);
state.cursor_position = state.graphemes.len();
set_new_cursor_position(
&mut state,
&font_assets,
&font_mapping,
&style_font,
);
EventType::Blur => {
if let Ok(mut state) = state_query.get_mut(state_entity) {
state.focused = false;
}
(event_dispatcher_context, event)
},
);
let cursor_styles = KStyle {
background_color: Color::rgba(0.933, 0.745, 0.745, 1.0).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()
};
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
let text_styles = KStyle {
top: Units::Stretch(1.0).into(),
bottom: Units::Stretch(1.0).into(),
..Default::default()
};
let shift = if let Some(layout) = widget_context.get_layout(entity) {
let font_handle = match &styles.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,
},
);
if measurement.size().0 > layout.width {
(layout.width - measurement.size().0) - 20.0
} else {
0.0
}
} else {
0.0
}
} else {
0.0
};
let scroll_styles = KStyle {
position_type: KPositionType::SelfDirected.into(),
padding_left: StyleProp::Value(Units::Stretch(0.0)),
padding_right: StyleProp::Value(Units::Stretch(0.0)),
padding_bottom: StyleProp::Value(Units::Stretch(1.0)),
padding_top: StyleProp::Value(Units::Stretch(1.0)),
left: Units::Pixels(shift).into(),
..Default::default()
};
let parent_id = Some(entity);
rsx! {
<BackgroundBundle styles={background_styles}>
<ClipBundle styles={KStyle {
height: Units::Pixels(26.0).into(),
padding_left: StyleProp::Value(Units::Stretch(0.0)),
padding_right: StyleProp::Value(Units::Stretch(0.0)),
..Default::default()
}}>
<ElementBundle styles={scroll_styles}>
<TextWidgetBundle
text={TextProps {
content: text_box.value.clone(),
size: 14.0,
line_height: Some(18.0),
word_wrap: false,
}}
/>
{
if state.focused && state.cursor_visible {
constructor! {
<BackgroundBundle styles={cursor_styles} />
}
</ClipBundle>
</BackgroundBundle>
/// Checks if the given character contains the "Backspace" sequence
///
/// Context: [Wikipedia](https://en.wikipedia.org/wiki/Backspace#Common_use)
fn is_backspace(c: char) -> bool {
c == '\u{8}' || c == '\u{7f}'
fn set_graphemes(
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) {
state.graphemes = font
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
}
}
fn get_single_grapheme_length(
font_assets: &Res<Assets<KayakFont>>,
font_mapping: &FontMapping,
style_font: &StyleProp<String>,
text: &String,
) -> usize {
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 graphemes = font.get_graphemes(&text);
return graphemes[0].len();
}
0
}
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
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;
}
}
}