Newer
Older
#![allow(clippy::too_many_arguments, clippy::type_complexity)]
use crate::{
buffer::{get_x_offset_center, get_y_offset_center},
cosmic_edit::{CosmicTextAlign, MaxChars, MaxLines, ReadOnly, ScrollEnabled, XOffset},
events::CosmicTextChanged,
prelude::*,
CosmicWidgetSize,
};
input::{
keyboard::{Key, KeyboardInput},
mouse::{MouseMotion, MouseScrollUnit, MouseWheel},
},
use cosmic_text::{Action, Cursor, Edit, Motion, Selection};
use bevy::tasks::AsyncComputeTaskPool;
#[cfg(target_arch = "wasm32")]
use js_sys::Promise;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::JsFuture;
/// System set for mouse and keyboard input events. Runs in [`PreUpdate`] and [`Update`]
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct InputSet;
pub(crate) struct InputPlugin;
impl Plugin for InputPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreUpdate, input_mouse.in_set(InputSet))
.add_systems(
Update,
(kb_move_cursor, kb_input_text, kb_clipboard)
.chain()
.in_set(InputSet),
)
.insert_resource(ClickTimer(Timer::from_seconds(0.5, TimerMode::Once)));
#[cfg(target_arch = "wasm32")]
{
let (tx, rx) = crossbeam_channel::bounded::<WasmPaste>(1);
app.insert_resource(WasmPasteAsyncChannel { tx, rx })
.add_systems(Update, poll_wasm_paste);
}
// TODO: hide this behind #cfg wasm, depends on wasm having own copy/paste fn
/// Crossbeam channel struct for Wasm clipboard data
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
pub(crate) struct WasmPaste {
/// Async channel for receiving from the clipboard in Wasm
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
pub tx: crossbeam_channel::Sender<WasmPaste>,
pub rx: crossbeam_channel::Receiver<WasmPaste>,
}
keys: Res<ButtonInput<KeyCode>>,
buttons: Res<ButtonInput<MouseButton>>,
mut font_system: ResMut<CosmicFontSystem>,
mut scroll_evr: EventReader<MouseWheel>,
camera_q: Query<(&Camera, &GlobalTransform)>,
mut click_timer: ResMut<ClickTimer>,
mut click_count: Local<usize>,
time: Res<Time>,
evr_mouse_motion: EventReader<MouseMotion>,
click_timer.0.tick(time.delta());
if click_timer.0.finished() || !evr_mouse_motion.is_empty() {
*click_count = 0;
}
if buttons.just_pressed(MouseButton::Left) {
click_timer.0.reset();
*click_count += 1;
}
if *click_count > 3 {
*click_count = 0;
}
// unwrap resources
let Some(active_editor_entity) = active_editor.0 else {
return;
};

Grim
committed
let Ok(primary_window) = windows.get_single() else {

Grim
committed
};

Grim
committed
let Some((camera, camera_transform)) = camera_q.iter().find(|(c, _)| c.is_active) else {
return;
};
// TODO: generalize this over UI and sprite
if let Ok((mut editor, transform, text_position, x_offset, scroll_disabled, target_size)) =
editor_q.get_mut(active_editor_entity)
let buffer = editor.with_buffer(|b| b.clone());
// get size of render target
let Ok(source_type) = target_size.scan() else {
return;
};
let Ok(size) = target_size.logical_size() else {
return;
};
let (width, height) = (size.x, size.y);
let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
// if shift key is pressed
let already_has_selection = editor.selection() != Selection::None;
let cursor = editor.cursor();
editor.set_selection(Selection::Normal(cursor));
}
let (padding_x, padding_y) = match text_position {
CosmicTextAlign::Center { padding: _ } => (
get_x_offset_center(width * scale_factor, &buffer),
get_y_offset_center(height * scale_factor, &buffer),
CosmicTextAlign::TopLeft { padding } => (*padding, *padding),
CosmicTextAlign::Left { padding } => (
get_y_offset_center(height * scale_factor, &buffer),
// Converts a node-relative space coordinate to a screen space physical coord
let screen_physical = |node_cursor_pos: Vec2| {
(node_cursor_pos.x * scale_factor) as i32 - padding_x,
(node_cursor_pos.y * scale_factor) as i32 - padding_y,
)
};
if buttons.just_pressed(MouseButton::Left) {
editor.cursor_visible = true;
editor.cursor_timer.reset();
if let Some(node_cursor_pos) = crate::render_targets::get_node_cursor_pos(
let (mut x, y) = screen_physical(node_cursor_pos);
editor.action(&mut font_system.0, Action::Drag { x, y });
editor.action(&mut font_system.0, Action::Click { x, y });
editor.action(&mut font_system.0, Action::Motion(Motion::LeftWord));
let cursor = editor.cursor();
editor.set_selection(Selection::Normal(cursor));
editor.action(&mut font_system.0, Action::Motion(Motion::RightWord));
editor
.action(&mut font_system.0, Action::Motion(Motion::ParagraphStart));
let cursor = editor.cursor();
editor.set_selection(Selection::Normal(cursor));
editor.action(&mut font_system.0, Action::Motion(Motion::ParagraphEnd));
if buttons.pressed(MouseButton::Left) && *click_count == 0 {
if let Some(node_cursor_pos) = crate::render_targets::get_node_cursor_pos(
let (mut x, y) = screen_physical(node_cursor_pos);
editor.action(&mut font_system.0, Action::Click { x, y });
editor.action(&mut font_system.0, Action::Drag { x, y });
for ev in scroll_evr.read() {
match ev.unit {
MouseScrollUnit::Line => {
editor.action(
&mut font_system.0,
Action::Scroll {
lines: -ev.y as i32,
},
);
}
MouseScrollUnit::Pixel => {
let line_height = buffer.metrics().line_height;
editor.action(
&mut font_system.0,
Action::Scroll {
lines: -(ev.y / line_height) as i32,
},
);
}
mut cosmic_edit_query: Query<(&mut CosmicEditor,)>,

Grim
committed
let Some(active_editor_entity) = active_editor.0 else {
return;
};
if let Ok((mut editor,)) = cosmic_edit_query.get_mut(active_editor_entity) {
if keys.get_just_pressed().len() != 0 {
editor.cursor_visible = true;
editor.cursor_timer.reset();
}
#[cfg(target_arch = "wasm32")]
let command = if web_sys::window()
.unwrap()
.navigator()
.user_agent()
.unwrap_or("NoUA".into())
.contains("Macintosh")
{
keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight])
} else {
command
};
#[cfg(target_os = "macos")]
let option = keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]);
let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
let already_has_selection = editor.selection() != Selection::None;
let cursor = editor.cursor();
editor.set_selection(Selection::Normal(cursor));
}
#[cfg(target_os = "macos")]
let should_jump = command && option;
#[cfg(not(target_os = "macos"))]
let should_jump = command;
if should_jump && keys.just_pressed(KeyCode::ArrowLeft) {
editor.action(&mut font_system.0, Action::Motion(Motion::PreviousWord));
if should_jump && keys.just_pressed(KeyCode::ArrowRight) {
editor.action(&mut font_system.0, Action::Motion(Motion::NextWord));
}
return;
}
if should_jump && keys.just_pressed(KeyCode::Home) {
editor.action(&mut font_system.0, Action::Motion(Motion::BufferStart));
}
return;
}
if should_jump && keys.just_pressed(KeyCode::End) {
editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd));
editor.action(&mut font_system.0, Action::Motion(Motion::Left));
editor.action(&mut font_system.0, Action::Motion(Motion::Right));
editor.action(&mut font_system.0, Action::Motion(Motion::Up));
editor.action(&mut font_system.0, Action::Motion(Motion::Down));
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
if keys.just_pressed(KeyCode::Escape) {
editor.action(&mut font_system.0, Action::Escape);
}
if command && keys.just_pressed(KeyCode::KeyA) {
editor.action(&mut font_system.0, Action::Motion(Motion::BufferEnd));
let current_cursor = editor.cursor();
editor.set_selection(Selection::Normal(Cursor {
line: 0,
index: 0,
affinity: current_cursor.affinity,
}));
return;
}
if keys.just_pressed(KeyCode::Home) {
editor.action(&mut font_system.0, Action::Motion(Motion::Home));
if !shift {
editor.set_selection(Selection::None);
}
return;
}
if keys.just_pressed(KeyCode::End) {
editor.action(&mut font_system.0, Action::Motion(Motion::End));
if !shift {
editor.set_selection(Selection::None);
}
return;
}
if keys.just_pressed(KeyCode::PageUp) {
editor.action(&mut font_system.0, Action::Motion(Motion::PageUp));
if !shift {
editor.set_selection(Selection::None);
}
return;
}
if keys.just_pressed(KeyCode::PageDown) {
editor.action(&mut font_system.0, Action::Motion(Motion::PageDown));
if !shift {
editor.set_selection(Selection::None);
}
}
}
}
pub(crate) fn kb_input_text(
active_editor: Res<FocusedWidget>,
keys: Res<ButtonInput<KeyCode>>,
mut char_evr: EventReader<KeyboardInput>,
mut cosmic_edit_query: Query<(
&mut CosmicEditor,
Entity,
Option<&ReadOnly>,
)>,
mut evw_changed: EventWriter<CosmicTextChanged>,
mut font_system: ResMut<CosmicFontSystem>,
mut is_deleting: Local<bool>,
) {
let Some(active_editor_entity) = active_editor.0 else {
return;
};
if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt)) =
cosmic_edit_query.get_mut(active_editor_entity)
{
let command = keypress_command(&keys);
if keys.get_just_pressed().len() != 0 {
editor.cursor_visible = true;
editor.cursor_timer.reset();
}
let readonly = readonly_opt.is_some();
match select {
Selection::Line(cursor) => {
if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
{
editor.set_selection(Selection::None);
}
}
Selection::Normal(cursor) => {
if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
{
editor.set_selection(Selection::None);
}
}
Selection::Word(cursor) => {
if editor.cursor().line == cursor.line && editor.cursor().index == cursor.index
{
editor.set_selection(Selection::None);
}
}
Selection::None => {}
if keys.just_pressed(KeyCode::Delete) && !readonly {
editor.action(&mut font_system.0, Action::Delete);
editor.with_buffer_mut(|b| b.set_redraw(true));
let mut is_edit = false;
let mut is_return = false;
if keys.just_pressed(KeyCode::Enter) {
is_return = true;
if (max_lines.0 == 0 || buffer.lines.len() < max_lines.0)
&& (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0)
{
// to have new line on wasm rather than E
is_edit = true;
editor.action(&mut font_system.0, Action::Insert('\n'));
if !is_return {
for char_ev in char_evr.read() {
is_edit = true;
if *is_deleting {
editor.action(&mut font_system.0, Action::Backspace);
} else if !command
&& (max_chars.0 == 0 || buffer.get_text().len() < max_chars.0)
&& matches!(char_ev.state, bevy::input::ButtonState::Pressed)
{
match &char_ev.logical_key {
Key::Character(char) => {
let b = char.as_bytes();
for c in b {
let c: char = (*c).into();
editor.action(&mut font_system.0, Action::Insert(c));
}
Key::Space => {
editor.action(&mut font_system.0, Action::Insert(' '));
}
_ => (),

sam edelsten
committed
evw_changed.send(CosmicTextChanged((
entity,
editor.with_buffer_mut(|b| b.get_text()),
)));
active_editor: Res<FocusedWidget>,
keys: Res<ButtonInput<KeyCode>>,
mut evw_changed: EventWriter<CosmicTextChanged>,
mut font_system: ResMut<CosmicFontSystem>,
mut cosmic_edit_query: Query<(
&mut CosmicEditor,
Entity,
Option<&ReadOnly>,
)>,
) {
let Some(active_editor_entity) = active_editor.0 else {
return;
};
if let Ok((mut editor, buffer, max_lines, max_chars, entity, readonly_opt)) =
cosmic_edit_query.get_mut(active_editor_entity)
{
let command = keypress_command(&keys);
let readonly = readonly_opt.is_some();
let mut is_clipboard = false;
#[cfg(not(target_arch = "wasm32"))]
{
if let Ok(mut clipboard) = arboard::Clipboard::new() {
clipboard.set_text(text).unwrap();
return;
}
}
if command && keys.just_pressed(KeyCode::KeyX) && !readonly {
if command && keys.just_pressed(KeyCode::KeyV) && !readonly {
if let Ok(text) = clipboard.get_text() {
for c in text.chars() {
if max_chars.0 == 0 || buffer.get_text().len() < max_chars.0 {
if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 {
editor.action(&mut font_system.0, Action::Insert(c));
editor.action(&mut font_system.0, Action::Insert(c));
}
}
}
}
is_clipboard = true;
}
}
}
write_clipboard_wasm(text.as_str());
return;
}
}
if command && keys.just_pressed(KeyCode::KeyX) && !readonly {
if command && keys.just_pressed(KeyCode::KeyV) && !readonly {
let tx = _channel.unwrap().tx.clone();
let _task = AsyncComputeTaskPool::get().spawn(async move {
let promise = read_clipboard_wasm();
let result = JsFuture::from(promise).await;
if let Ok(js_text) = result {
if let Some(text) = js_text.as_string() {
let _ = tx.try_send(WasmPaste { text, entity });
}
}
});
return;
}
}
evw_changed.send(CosmicTextChanged((entity, buffer.get_text())));
fn keypress_command(keys: &ButtonInput<KeyCode>) -> bool {
#[cfg(target_os = "macos")]
let command = keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]);
#[cfg(not(target_os = "macos"))]
let command = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
#[cfg(target_arch = "wasm32")]
let command = if web_sys::window()
.unwrap()
.navigator()
.user_agent()
.unwrap_or("NoUA".into())
.contains("Macintosh")
{
keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight])
} else {
command
};
command
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn write_clipboard_wasm(text: &str) {
let clipboard = web_sys::window().unwrap().navigator().clipboard();
let _result = clipboard.write_text(text);
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn read_clipboard_wasm() -> Promise {
let clipboard = web_sys::window().unwrap().navigator().clipboard();
clipboard.read_text()
}
#[cfg(target_arch = "wasm32")]
channel: Res<WasmPasteAsyncChannel>,
mut editor_q: Query<
(
&mut CosmicEditor,
),
Without<ReadOnly>,
>,
mut evw_changed: EventWriter<CosmicTextChanged>,
mut font_system: ResMut<CosmicFontSystem>,
) {
let inlet = channel.rx.try_recv();
match inlet {
Ok(inlet) => {
let entity = inlet.entity;
if let Ok((mut editor, buffer, max_chars, max_lines)) = editor_q.get_mut(entity) {
let text = inlet.text;
for c in text.chars() {
if max_chars.0 == 0 || buffer.get_text().len() < max_chars.0 {
if max_lines.0 == 0 || buffer.lines.len() < max_lines.0 {
editor.action(&mut font_system.0, Action::Insert(c));
editor.action(&mut font_system.0, Action::Insert(c));
evw_changed.send(CosmicTextChanged((entity, buffer.get_text())));