Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions packages/blitz-dom/src/events/keyboard.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::{BaseDocument, node::GeneratedTextInputEvent, util::ACTION_MOD};
use crate::{
BaseDocument,
node::GeneratedTextInputEvent,
util::{ACTION_MOD, action_key},
};
use blitz_traits::node_id::NodeId;
use blitz_traits::{
SmolStr,
Expand Down Expand Up @@ -31,22 +35,18 @@ pub(crate) fn handle_key_or_input_event<F: FnMut(DomEvent)>(
// Handle copy (Ctrl+C/Cmd+C) for text selection when no text input is focused
if event.state.is_pressed() {
let action_mod = event.modifiers.contains(ACTION_MOD);
if action_mod {
if let Key::Character(c) = &event.key {
if c.to_lowercase() == "c" {
// Check if we have a text selection (and no focused text input)
let has_focused_text_input = doc.focus_node_id.is_some_and(|id| {
doc.get_node(id)
.and_then(|n| n.element_data())
.is_some_and(|e| e.text_input_data().is_some())
});
if action_mod && action_key(event) == Some('c') {
// Check if we have a text selection (and no focused text input)
let has_focused_text_input = doc.focus_node_id.is_some_and(|id| {
doc.get_node(id)
.and_then(|n| n.element_data())
.is_some_and(|e| e.text_input_data().is_some())
});

if !has_focused_text_input {
if let Some(text) = doc.get_selected_text() {
let _ = doc.shell_provider.set_clipboard_text(text);
return;
}
}
if !has_focused_text_input {
if let Some(text) = doc.get_selected_text() {
let _ = doc.shell_provider.set_clipboard_text(text);
return;
}
}
}
Expand Down
242 changes: 164 additions & 78 deletions packages/blitz-dom/src/node/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use blitz_traits::{
use keyboard_types::{Key, Modifiers};
use parley::{ContentWidths, FontContext, LayoutContext};

use crate::util::ACTION_MOD;
use crate::util::{ACTION_MOD, action_key};

#[derive(Debug, Clone, Copy, Default, PartialEq)]
/// Parley Brush type for Blitz which contains the Blitz node id
Expand Down Expand Up @@ -211,128 +211,195 @@ impl TextInputData {
let is_multiline = self.is_multiline;
let editor = &mut self.editor;
let mut driver = editor.driver(font_ctx, layout_ctx);
match event.key {
Key::Character(c) if action_mod && matches!(c.as_str(), "c" | "x" | "v") => {
match c.to_lowercase().as_str() {
"c" => {
if let Some(text) = driver.editor.selected_text() {
let _ = shell_provider.set_clipboard_text(text.to_owned());
}
}
"x" => {
if let Some(text) = driver.editor.selected_text() {
let _ = shell_provider.set_clipboard_text(text.to_owned());
driver.delete_selection()
}

if action_mod {
match action_key(&event) {
Some('c') => {
if let Some(text) = driver.editor.selected_text() {
let _ = shell_provider.set_clipboard_text(text.to_owned());
}
"v" => {
let text = shell_provider.get_clipboard_text().unwrap_or_default();
driver.insert_or_replace_selection(&text)
return None;
}
Some('x') => {
if let Some(text) = driver.editor.selected_text() {
let _ = shell_provider.set_clipboard_text(text.to_owned());
driver.delete_selection();
return Some(GeneratedTextInputEvent::Input);
}
_ => unreachable!(),
return None;
}

return Some(GeneratedTextInputEvent::Input);
}
Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => {
if shift {
driver.collapse_selection()
} else {
driver.select_all()
Some('v') => {
let text = shell_provider.get_clipboard_text().unwrap_or_default();
driver.insert_or_replace_selection(&text);
return Some(GeneratedTextInputEvent::Input);
}
return Some(GeneratedTextInputEvent::Select);
Some('a') => {
if shift {
driver.collapse_selection();
} else {
driver.select_all();
}
return Some(GeneratedTextInputEvent::Select);
}
_ => {}
}
}

match event.key {
Key::ArrowLeft => {
#[cfg(target_os = "macos")]
if action_mod {
if shift {
driver.select_word_left()
driver.select_to_line_start();
} else {
driver.move_word_left()
driver.move_to_line_start();
}
} else if shift {
driver.select_left()
} else {
driver.move_left()
return Some(GeneratedTextInputEvent::Select);
}

#[cfg(not(target_os = "macos"))]
{
if action_mod {
if shift {
driver.select_word_left();
} else {
driver.move_word_left();
}
} else if shift {
driver.select_left();
} else {
driver.move_left();
}
return Some(GeneratedTextInputEvent::Select);
}
return Some(GeneratedTextInputEvent::Select);
}
Key::ArrowRight => {
#[cfg(target_os = "macos")]
if action_mod {
if shift {
driver.select_word_right()
driver.select_to_line_end();
} else {
driver.move_word_right()
driver.move_to_line_end();
}
} else if shift {
driver.select_right()
} else {
driver.move_right()
return Some(GeneratedTextInputEvent::Select);
}

#[cfg(not(target_os = "macos"))]
{
if action_mod {
if shift {
driver.select_word_right();
} else {
driver.move_word_right();
}
} else if shift {
driver.select_right();
} else {
driver.move_right();
}
return Some(GeneratedTextInputEvent::Select);
}
return Some(GeneratedTextInputEvent::Select);
}
Key::ArrowUp => {
if shift {
driver.select_up()
} else {
driver.move_up()
#[cfg(target_os = "macos")]
if action_mod {
if shift {
driver.select_to_text_start();
} else {
driver.move_to_text_start();
}
return Some(GeneratedTextInputEvent::Select);
}

#[cfg(not(target_os = "macos"))]
{
if shift {
driver.select_up();
} else {
driver.move_up();
}
return Some(GeneratedTextInputEvent::Select);
}
return Some(GeneratedTextInputEvent::Select);
}
Key::ArrowDown => {
if shift {
driver.select_down()
#[cfg(target_os = "macos")]
if action_mod {
if shift {
driver.select_to_text_end();
} else {
driver.move_to_text_end();
}
return Some(GeneratedTextInputEvent::Select);
}

#[cfg(not(target_os = "macos"))]
{
if shift {
driver.select_down();
} else {
driver.move_down();
}
return Some(GeneratedTextInputEvent::Select);
}
}
Key::Backspace => {
#[cfg(target_os = "macos")]
if action_mod {
if driver.editor.raw_selection().is_collapsed() {
driver.select_to_line_start();
}
driver.delete_selection();
return Some(GeneratedTextInputEvent::Input);
}

#[cfg(not(target_os = "macos"))]
{
if action_mod {
driver.backdelete_word();
} else {
driver.backdelete();
}
return Some(GeneratedTextInputEvent::Input);
}
}
// On macOS this is handled by the apple standard keybindings
#[cfg(not(target_os = "macos"))]
Key::Delete => {
if action_mod {
driver.delete_word();
} else {
driver.move_down()
driver.delete();
}
return Some(GeneratedTextInputEvent::Select);
return Some(GeneratedTextInputEvent::Input);
}
Key::Home => {
if action_mod {
if shift {
driver.select_to_text_start()
driver.select_to_text_start();
} else {
driver.move_to_text_start()
driver.move_to_text_start();
}
} else if shift {
driver.select_to_line_start()
driver.select_to_line_start();
} else {
driver.move_to_line_start()
driver.move_to_line_start();
}
return Some(GeneratedTextInputEvent::Select);
}
Key::End => {
if action_mod {
if shift {
driver.select_to_text_end()
driver.select_to_text_end();
} else {
driver.move_to_text_end()
driver.move_to_text_end();
}
} else if shift {
driver.select_to_line_end()
driver.select_to_line_end();
} else {
driver.move_to_line_end()
driver.move_to_line_end();
}
return Some(GeneratedTextInputEvent::Select);
}
Key::Delete => {
if action_mod {
driver.delete_word()
} else {
driver.delete()
}
return Some(GeneratedTextInputEvent::Input);
}

// On macOS this is handled by the apple standard keybindings
#[cfg(not(target_os = "macos"))]
Key::Backspace => {
if action_mod {
driver.backdelete_word()
} else {
driver.backdelete()
}
return Some(GeneratedTextInputEvent::Input);
}

Key::Character(c) if c == "\n" => {
if is_multiline {
Expand Down Expand Up @@ -482,6 +549,25 @@ impl TextInputData {
return Some(GeneratedTextInputEvent::Input);
}
}
"copy:" => {
if let Some(text) = driver.editor.selected_text() {
let _ = shell_provider.set_clipboard_text(text.to_owned());
}
return None;
}
"cut:" => {
if let Some(text) = driver.editor.selected_text() {
let _ = shell_provider.set_clipboard_text(text.to_owned());
driver.delete_selection();
return Some(GeneratedTextInputEvent::Input);
}
return None;
}
"paste:" => {
let text = shell_provider.get_clipboard_text().unwrap_or_default();
driver.insert_or_replace_selection(&text);
return Some(GeneratedTextInputEvent::Input);
}

// Moving the Insertion Pointer

Expand Down Expand Up @@ -584,7 +670,7 @@ impl TextInputData {
return Some(GeneratedTextInputEvent::Select);
}
"moveToEndOfDocumentAndModifySelection:" => {
driver.move_to_text_end();
driver.select_to_text_end();
return Some(GeneratedTextInputEvent::Select);
}

Expand Down
26 changes: 26 additions & 0 deletions packages/blitz-dom/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ pub(crate) const ACTION_MOD: Modifiers = Modifiers::SUPER;
#[cfg(not(target_os = "macos"))]
pub(crate) const ACTION_MOD: Modifiers = Modifiers::CONTROL;

/// Normalizes a key event triggered with an action modifier (Cmd on macOS, Ctrl on others)
/// to a lowercase ASCII character, prioritizing physical key code (Code) over layout-specific
/// logical characters (Key).
pub(crate) fn action_key(event: &blitz_traits::events::BlitzKeyEvent) -> Option<char> {
use keyboard_types::{Code, Key};

match event.code {
Code::KeyA => Some('a'),
Code::KeyC => Some('c'),
Code::KeyV => Some('v'),
Code::KeyX => Some('x'),
Code::KeyZ => Some('z'),
_ => match &event.key {
Key::Character(c) if c.len() == 1 => {
let ch = c.chars().next()?;
if ch.is_ascii_alphabetic() {
Some(ch.to_ascii_lowercase())
} else {
None
}
}
_ => None,
},
}
}

pub type Color = AlphaColor<Srgb>;

/// Decode raw font bytes, decompressing WOFF/WOFF2 if the `woff` feature is enabled.
Expand Down
Loading