diff --git a/packages/blitz-dom/src/events/driver.rs b/packages/blitz-dom/src/events/driver.rs index 13d0aea20..55a3bcb1f 100644 --- a/packages/blitz-dom/src/events/driver.rs +++ b/packages/blitz-dom/src/events/driver.rs @@ -182,6 +182,8 @@ impl<'doc, Handler: EventHandler> EventDriver<'doc, Handler> { UiEvent::KeyUp(_) => focussed_node_id, UiEvent::KeyDown(_) => focussed_node_id, UiEvent::Ime(_) => focussed_node_id, + UiEvent::ClipboardPaste(_) => focussed_node_id, + UiEvent::ClipboardCopy | UiEvent::ClipboardCut => focussed_node_id, }; let target = target.unwrap_or_else(|| self.doc.inner().root_element().id); @@ -222,6 +224,15 @@ impl<'doc, Handler: EventHandler> EventDriver<'doc, Handler> { UiEvent::Ime(data) => { self.handle_dom_event(DomEvent::new(target, DomEventData::Ime(data))) } + UiEvent::ClipboardPaste(data) => { + self.handle_dom_event(DomEvent::new(target, DomEventData::Paste(data))) + } + UiEvent::ClipboardCopy => { + self.handle_dom_event(DomEvent::new(target, DomEventData::Copy)) + } + UiEvent::ClipboardCut => { + self.handle_dom_event(DomEvent::new(target, DomEventData::Cut)) + } }; // Update document input state (hover, focus, active, etc) diff --git a/packages/blitz-dom/src/events/keyboard.rs b/packages/blitz-dom/src/events/keyboard.rs index d3611384a..ae4cdcde0 100644 --- a/packages/blitz-dom/src/events/keyboard.rs +++ b/packages/blitz-dom/src/events/keyboard.rs @@ -74,7 +74,11 @@ pub(crate) fn handle_keypress( if let Some(generated_event) = generated_event { match generated_event { GeneratedEvent::Input => { - let value = input_data.editor.raw_text().to_string(); + let value = if input_data.is_password { + input_data.shadow_text.clone() + } else { + input_data.editor.raw_text().to_string() + }; dispatch_event(DomEvent::new( node_id, DomEventData::Input(BlitzInputEvent { value }), @@ -141,14 +145,7 @@ fn apply_keypress_event( return Some(GeneratedEvent::Input); } - Key::Character(c) if action_mod && matches!(c.to_lowercase().as_str(), "a") => { - if shift { - driver.collapse_selection() - } else { - driver.select_all() - } - return Some(GeneratedEvent::Select); - } + Key::ArrowLeft => { if action_mod { if shift { @@ -222,6 +219,9 @@ fn apply_keypress_event( return Some(GeneratedEvent::Select); } Key::Delete => { + if input_data.is_password { + sync_shadow_before_edit(&mut input_data.shadow_text, &driver.editor); + } if action_mod { driver.delete_word() } else { @@ -230,6 +230,9 @@ fn apply_keypress_event( return Some(GeneratedEvent::Input); } Key::Backspace => { + if input_data.is_password { + sync_shadow_before_edit(&mut input_data.shadow_text, &driver.editor); + } if action_mod { driver.backdelete_word() } else { @@ -254,8 +257,17 @@ fn apply_keypress_event( } } Key::Character(s) => { + if input_data.is_password { + let selection = driver.editor.raw_selection(); + if selection.anchor() != selection.focus() { + input_data.shadow_text.clear(); + } + input_data.shadow_text.push_str(&s); + driver.insert_or_replace_selection("•"); + } else { driver.insert_or_replace_selection(&s); - return Some(GeneratedEvent::Input); + } + return Some(GeneratedEvent::Input); } _ => {} }; @@ -300,3 +312,14 @@ fn implicit_form_submission(doc: &BaseDocument, text_target: usize) { doc.submit_form(*form_owner_id, *form_owner_id); } + +fn sync_shadow_before_edit(shadow_text: &mut String, editor: &parley::PlainEditor) { + let selection = editor.raw_selection(); + if selection.anchor() == selection.focus() { + if !shadow_text.is_empty() { + shadow_text.pop(); + } + } else { + shadow_text.clear(); + } +} diff --git a/packages/blitz-dom/src/events/mod.rs b/packages/blitz-dom/src/events/mod.rs index d2215cf75..9a9362417 100644 --- a/packages/blitz-dom/src/events/mod.rs +++ b/packages/blitz-dom/src/events/mod.rs @@ -88,6 +88,10 @@ pub(crate) fn handle_dom_event( DomEventData::Blur(_) => None, DomEventData::FocusIn(_) => None, DomEventData::FocusOut(_) => None, + + DomEventData::Paste(data) => Some(UiEvent::ClipboardPaste(data)), + DomEventData::Copy => Some(UiEvent::ClipboardCopy), + DomEventData::Cut => Some(UiEvent::ClipboardCut) }; if let Some(ui_event) = ui_event { @@ -202,5 +206,6 @@ pub(crate) fn handle_dom_event( DomEventData::FocusOut(_) => { // Do nothing (no default action) } + DomEventData::Paste(_) | DomEventData::Copy | DomEventData::Cut => {} } } diff --git a/packages/blitz-dom/src/form.rs b/packages/blitz-dom/src/form.rs index bb44d1455..778dc562c 100644 --- a/packages/blitz-dom/src/form.rs +++ b/packages/blitz-dom/src/form.rs @@ -313,10 +313,13 @@ fn construct_entry_list(doc: &BaseDocument, form_id: usize, submitter_id: usize) create_entry(name, charset.into()); } // Otherwise, create an entry with name and the value of the field element, and append it to entry list. - else if let Some(text) = element.text_input_data() { - create_entry(name, text.editor.text().to_string().as_str().into()); - } else if let Some(value) = element.attr(local_name!("value")) { - create_entry(name, value.into()); + else if let Some(text) = element.text_input_data() { + let value = if text.is_password { + text.shadow_text.clone() + } else { + text.editor.text().to_string() + }; + create_entry(name, value.as_str().into()); } } entry_list diff --git a/packages/blitz-dom/src/layout/construct.rs b/packages/blitz-dom/src/layout/construct.rs index 89f5162c3..241385c7a 100644 --- a/packages/blitz-dom/src/layout/construct.rs +++ b/packages/blitz-dom/src/layout/construct.rs @@ -613,8 +613,21 @@ fn create_text_editor(doc: &mut BaseDocument, input_element_id: usize, is_multil let element = &mut node.data.downcast_element_mut().unwrap(); if !matches!(element.special_data, SpecialElementData::TextInput(_)) { let mut text_input_data = TextInputData::new(is_multiline); + + if element.attr(local_name!("type")) == Some("password") { + text_input_data.is_password = true; + } let editor = &mut text_input_data.editor; - editor.set_text(element.attr(local_name!("value")).unwrap_or(" ")); + + // logic for the masking... + let initial_text = element.attr(local_name!("value")).unwrap_or(""); + if text_input_data.is_password { + text_input_data.shadow_text = initial_text.to_string(); + editor.set_text(&"•".repeat(initial_text.chars().count())); + } else { + editor.set_text(initial_text); + } + element.special_data = SpecialElementData::TextInput(text_input_data); } diff --git a/packages/blitz-dom/src/node/element.rs b/packages/blitz-dom/src/node/element.rs index 7ff28c830..d9ea7ffa5 100644 --- a/packages/blitz-dom/src/node/element.rs +++ b/packages/blitz-dom/src/node/element.rs @@ -490,26 +490,22 @@ impl BackgroundImageData { } } +#[derive(Clone)] pub struct TextInputData { - /// A parley TextEditor instance pub editor: Box>, - /// Whether the input is a singleline or multiline input pub is_multiline: bool, -} - -// FIXME: Implement Clone for PlainEditor -impl Clone for TextInputData { - fn clone(&self) -> Self { - TextInputData::new(self.is_multiline) - } + pub is_password: bool, + // this is plaintext string + pub shadow_text: String, } impl TextInputData { pub fn new(is_multiline: bool) -> Self { - let editor = Box::new(parley::PlainEditor::new(16.0)); Self { - editor, + editor: Box::new(parley::PlainEditor::new(16.0)), is_multiline, + is_password: false, + shadow_text: String::new(), } } @@ -519,8 +515,17 @@ impl TextInputData { layout_ctx: &mut LayoutContext, text: &str, ) { - if self.editor.text() != text { - self.editor.set_text(text); + self.shadow_text = text.to_string(); + + // show password + let display_text = if self.is_password { + "•".repeat(text.chars().count()) + } else { + text.to_string() + }; + + if self.editor.text() != display_text.as_str() { + self.editor.set_text(&display_text); self.editor.driver(font_ctx, layout_ctx).refresh_layout(); } } diff --git a/packages/blitz-shell/Cargo.toml b/packages/blitz-shell/Cargo.toml index 989840b38..cc7c059d0 100644 --- a/packages/blitz-shell/Cargo.toml +++ b/packages/blitz-shell/Cargo.toml @@ -44,9 +44,12 @@ data-url = { workspace = true, optional = true } [target.'cfg(target_os = "android")'.dependencies] android-activity = { version = "0.6.0" } -[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] -arboard = { workspace = true, optional = true } +[target.'cfg(target_os = "linux")'.dependencies] +arboard = { workspace = true, optional = true, features = ["wayland-data-control"] } +# for other platforms +[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +arboard = { workspace = true, optional = true } [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies] rfd = { workspace = true, optional = true, features = ["xdg-portal"] } diff --git a/packages/blitz-shell/src/application.rs b/packages/blitz-shell/src/application.rs index 2bd6bdd71..44d95ce75 100644 --- a/packages/blitz-shell/src/application.rs +++ b/packages/blitz-shell/src/application.rs @@ -117,7 +117,7 @@ impl ApplicationHandler for BlitzApplication { // TODO } - fn window_event( + fn window_event( &mut self, event_loop: &dyn ActiveEventLoop, window_id: WindowId, diff --git a/packages/blitz-shell/src/window.rs b/packages/blitz-shell/src/window.rs index 2b42550a3..3eaf5efd9 100644 --- a/packages/blitz-shell/src/window.rs +++ b/packages/blitz-shell/src/window.rs @@ -53,13 +53,16 @@ impl WindowConfig { } pub struct View { + + //if we move this to the top it should be correct order. allowing surface to die. + pub window: Arc, + pub doc: Box, pub renderer: Rend, pub waker: Option, pub proxy: BlitzShellProxy, - pub window: Arc, /// The state of the keyboard modifiers (ctrl, shift, etc). Winit/Tao don't track these for us so we /// need to store them in order to have access to them when processing keypress events @@ -388,63 +391,60 @@ impl View { self.request_redraw(); }, WindowEvent::ModifiersChanged(new_state) => { - // Store new keyboard modifier (ctrl, shift, etc) state for later use self.keyboard_modifiers = new_state; } WindowEvent::KeyboardInput { event, .. } => { - - if let PhysicalKey::Code(key_code) = event.physical_key && event.state.is_pressed() { - let ctrl = self.keyboard_modifiers.state().control_key(); - let meta = self.keyboard_modifiers.state().meta_key(); - let alt = self.keyboard_modifiers.state().alt_key(); - - // Ctrl/Super keyboard shortcuts - if ctrl | meta { - match key_code { - KeyCode::Equal => { - self.doc.inner_mut().viewport_mut().zoom_by(0.1); - }, - KeyCode::Minus => { - self.doc.inner_mut().viewport_mut().zoom_by(-0.1); - }, - KeyCode::Digit0 => { - self.doc.inner_mut().viewport_mut().set_zoom(1.0); + let modifiers = self.keyboard_modifiers.state(); + let is_pressed = event.state.is_pressed(); + + if let PhysicalKey::Code(key_code) = event.physical_key && is_pressed { + let ctrl = modifiers.control_key(); + let meta = modifiers.meta_key(); + let alt = modifiers.alt_key(); + + if ctrl | meta { + match key_code { + KeyCode::Equal => self.doc.inner_mut().viewport_mut().zoom_by(0.1), + KeyCode::Minus => self.doc.inner_mut().viewport_mut().zoom_by(-0.1), + KeyCode::Digit0 => self.doc.inner_mut().viewport_mut().set_zoom(1.0), + KeyCode::KeyV => { + let shell = self.doc.inner().shell_provider.clone(); + if let Ok(text) = shell.get_clipboard_text() { + let event = blitz_traits::events::BlitzClipboardEvent { content: text }; + self.doc.handle_ui_event(UiEvent::ClipboardPaste(event)); } - _ => {} - }; + } + KeyCode::KeyC => { + println!("DEBUG: Copy Triggered"); + self.doc.handle_ui_event(UiEvent::ClipboardCopy); + } + KeyCode::KeyX => { + println!("DEBUG: Cut Triggered"); + self.doc.handle_ui_event(UiEvent::ClipboardCut); + } + _ => {} } + self.request_redraw(); + } - // Alt keyboard shortcuts - if alt { - match key_code { - KeyCode::KeyD => { - let mut inner = self.doc.inner_mut(); - inner.devtools_mut().toggle_show_layout(); - drop(inner); - self.request_redraw(); - } - KeyCode::KeyH => { - let mut inner = self.doc.inner_mut(); - inner.devtools_mut().toggle_highlight_hover(); - drop(inner); - self.request_redraw(); - } - KeyCode::KeyT => self.doc.inner().print_taffy_tree(), - _ => {} - }; + if alt { + match key_code { + KeyCode::KeyD => self.doc.inner_mut().devtools_mut().toggle_show_layout(), + KeyCode::KeyH => self.doc.inner_mut().devtools_mut().toggle_highlight_hover(), + KeyCode::KeyT => self.doc.inner().print_taffy_tree(), + _ => {} } - + self.request_redraw(); + } } - // Unmodified keypresses - let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state()); - let event = if event.state.is_pressed() { + let key_event_data = winit_key_event_to_blitz(&event, modifiers); + let ui_event = if is_pressed { UiEvent::KeyDown(key_event_data) } else { UiEvent::KeyUp(key_event_data) }; - - self.doc.handle_ui_event(event); + self.doc.handle_ui_event(ui_event); } WindowEvent::PointerEntered { /*device_id*/.. } => {} WindowEvent::PointerLeft { /*device_id*/.. } => {} diff --git a/packages/blitz-traits/src/events.rs b/packages/blitz-traits/src/events.rs index cde32a5b4..6a7fb0fd8 100644 --- a/packages/blitz-traits/src/events.rs +++ b/packages/blitz-traits/src/events.rs @@ -12,6 +12,12 @@ pub struct EventState { propagation_stopped: bool, redraw_requested: bool, } + +#[derive(Clone, Debug)] +pub struct BlitzClipboardEvent { + pub content: String, // The text being pasted +} + impl EventState { #[inline(always)] pub fn prevent_default(&mut self) { @@ -63,6 +69,9 @@ pub enum UiEvent { KeyUp(BlitzKeyEvent), KeyDown(BlitzKeyEvent), Ime(BlitzImeEvent), + ClipboardPaste(BlitzClipboardEvent), + ClipboardCopy, + ClipboardCut, } impl UiEvent { pub fn discriminant(&self) -> u8 { @@ -139,6 +148,10 @@ pub enum DomEventKind { Blur, FocusIn, FocusOut, + + Copy, + Cut, + Paste, } impl DomEventKind { pub fn discriminant(self) -> u8 { @@ -182,6 +195,10 @@ impl FromStr for DomEventKind { "blur" => Ok(Self::Blur), "focusin" => Ok(Self::FocusIn), "focusout" => Ok(Self::FocusOut), + + "copy" => Ok(Self::Copy), + "cut" => Ok(Self::Cut), + "paste" => Ok(Self::Paste), _ => Err(()), } } @@ -223,6 +240,9 @@ pub enum DomEventData { Blur(BlitzFocusEvent), FocusIn(BlitzFocusEvent), FocusOut(BlitzFocusEvent), + Copy, + Cut, + Paste(BlitzClipboardEvent), } impl DomEventData { pub fn discriminant(&self) -> u8 { @@ -270,6 +290,10 @@ impl DomEventData { Self::Blur { .. } => "blur", Self::FocusIn { .. } => "focusin", Self::FocusOut { .. } => "focusout", + + Self::Copy => "copy", + Self::Cut => "cut", + Self::Paste { .. } => "paste", } } @@ -308,6 +332,10 @@ impl DomEventData { Self::Blur { .. } => DomEventKind::Blur, Self::FocusIn { .. } => DomEventKind::FocusIn, Self::FocusOut { .. } => DomEventKind::FocusOut, + + Self::Paste { .. } => DomEventKind::Paste, + Self::Copy => DomEventKind::Copy, + Self::Cut => DomEventKind::Cut, } } @@ -346,6 +374,8 @@ impl DomEventData { Self::Blur { .. } => false, Self::FocusIn { .. } => false, Self::FocusOut { .. } => false, + + Self::Copy | Self::Cut | Self::Paste { .. } => true, } } @@ -384,6 +414,8 @@ impl DomEventData { Self::Blur { .. } => false, Self::FocusIn { .. } => true, Self::FocusOut { .. } => true, + + Self::Copy | Self::Cut | Self::Paste { .. } => true, } } } diff --git a/packages/dioxus-native-dom/src/dioxus_document.rs b/packages/dioxus-native-dom/src/dioxus_document.rs index 7c01cd2ff..cdcae5ac4 100644 --- a/packages/dioxus-native-dom/src/dioxus_document.rs +++ b/packages/dioxus-native-dom/src/dioxus_document.rs @@ -311,6 +311,16 @@ impl EventHandler for DioxusEventHandler<'_> { values: vec![], })), + DomEventData::Paste(data) => { + Some(wrap_event_data(data.clone())) + } + + DomEventData::Copy | DomEventData::Cut => { + Some(wrap_event_data(blitz_traits::events::BlitzClipboardEvent { + content: String::new(), + })) +} + // TODO: Implement IME handling DomEventData::Ime(_) => None, }; diff --git a/packages/dioxus-native-dom/src/events.rs b/packages/dioxus-native-dom/src/events.rs index 4bc02ea98..a871fb475 100644 --- a/packages/dioxus-native-dom/src/events.rs +++ b/packages/dioxus-native-dom/src/events.rs @@ -14,7 +14,7 @@ use dioxus_html::{ InteractionElementOffset, InteractionLocation, ModifiersInteraction, PointerInteraction, }, AnimationData, CancelData, ClipboardData, CompositionData, DragData, FocusData, FormData, - FormValue, HasFileData, HasFocusData, HasFormData, HasKeyboardData, HasMouseData, + FormValue, HasFileData, HasFocusData, HasFormData, HasKeyboardData, HasMouseData, HasClipboardData, HasPointerData, HasScrollData, HasWheelData, HtmlEventConverter, ImageData, KeyboardData, MediaData, MountedData, MountedError, MountedResult, MouseData, PlatformEventData, PointerData, RenderedElementBacking, ResizeData, ScrollBehavior, ScrollData, ScrollToOptions, SelectionData, @@ -67,9 +67,14 @@ impl HtmlEventConverter for NativeConverter { unimplemented!("todo: convert_animation_data in dioxus-native. requires support in blitz") } - fn convert_clipboard_data(&self, _event: &PlatformEventData) -> ClipboardData { - unimplemented!("todo: convert_clipboard_data in dioxus-native. requires support in blitz") - } +fn convert_clipboard_data(&self, event: &PlatformEventData) -> ClipboardData { + let raw_event = event + .downcast::() + .unwrap() + .clone(); + + dioxus_html::ClipboardData::new(NativeClipboardData(raw_event)) +} fn convert_composition_data(&self, _event: &PlatformEventData) -> CompositionData { unimplemented!("todo: convert_composition_data in dioxus-native. requires support in blitz") @@ -506,3 +511,12 @@ impl InteractionLocation for NativeWheelData { PagePoint::new(self.0.page_x() as f64, self.0.page_y() as f64) } } + +#[derive(Clone, Debug)] +pub struct NativeClipboardData(pub blitz_traits::events::BlitzClipboardEvent); + +impl HasClipboardData for NativeClipboardData { + fn as_any(&self) -> &dyn Any { + self as &dyn Any + } +}