fix: Bildschirmtastatur unter GNOME/Wayland (Issue #21)
GNOME blendet die Bildschirmtastatur erst ein, wenn ein bereits aktives Textfeld zwp_text_input_v3.enable erneut meldet (wie GTK beim Antippen). winit meldet enable nur einmal. Ein eigenes text-input-Objekt auf derselben Wayland-Verbindung schickt das zweite enable, wenn ein Textfeld den Fokus bekommt oder erneut angetippt wird. - Zeichen der Bildschirmtastatur kommen als IME-Text ohne Vorschau; egui 0.27 fügte davon nur das erste ein. Sie werden jetzt als normaler Text übergeben. - Die Rücktaste löscht über delete_surrounding_text; dafür meldet das eigene Objekt einen Platzhalter-Umgebungstext und übersetzt das Löschen in Tastendrücke. - Ein Fingertipp ging verloren, wenn GNOME beim Antippen den Mauszeiger ausblendet (CursorLeft bei aufliegendem Finger); das Ereignis erreicht egui in dem Fall nicht mehr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+3
@@ -1418,6 +1418,9 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"ureq",
|
"ureq",
|
||||||
"url",
|
"url",
|
||||||
|
"wayland-backend",
|
||||||
|
"wayland-client",
|
||||||
|
"wayland-protocols",
|
||||||
"webbrowser",
|
"webbrowser",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winit",
|
"winit",
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ hayro-svg = "0.8"
|
|||||||
# Text-Werkzeug: Schriftmetriken und Rasterung (Schriften aus egui)
|
# Text-Werkzeug: Schriftmetriken und Rasterung (Schriften aus egui)
|
||||||
ab_glyph = "0.2"
|
ab_glyph = "0.2"
|
||||||
|
|
||||||
|
# Bildschirmtastatur unter Wayland anfordern (eigenes text-input-Objekt, siehe src/osk.rs)
|
||||||
|
wayland-client = "0.31"
|
||||||
|
wayland-backend = { version = "0.3", features = ["client_system"] }
|
||||||
|
wayland-protocols = { version = "0.31", features = ["client", "unstable"] }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1 # Erhöht Rendering-Performance auch im Debug-Modus
|
opt-level = 1 # Erhöht Rendering-Performance auch im Debug-Modus
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub mod geometry;
|
|||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod image_layer;
|
pub mod image_layer;
|
||||||
pub mod library;
|
pub mod library;
|
||||||
|
pub mod osk;
|
||||||
pub mod page_export;
|
pub mod page_export;
|
||||||
pub mod pdf_doc;
|
pub mod pdf_doc;
|
||||||
pub mod pdf_merge;
|
pub mod pdf_merge;
|
||||||
|
|||||||
+52
-2
@@ -12,6 +12,7 @@ use winit::{
|
|||||||
event_loop::{ControlFlow, EventLoopBuilder},
|
event_loop::{ControlFlow, EventLoopBuilder},
|
||||||
keyboard::{Key, ModifiersState, NamedKey},
|
keyboard::{Key, ModifiersState, NamedKey},
|
||||||
platform::{wayland::EventLoopBuilderExtWayland, x11::EventLoopBuilderExtX11},
|
platform::{wayland::EventLoopBuilderExtWayland, x11::EventLoopBuilderExtX11},
|
||||||
|
raw_window_handle::{HasDisplayHandle, RawDisplayHandle},
|
||||||
window::{Window, WindowBuilder},
|
window::{Window, WindowBuilder},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ use ink_notes::export::{self, export_notebook_to_pdf, ExportStrategy};
|
|||||||
use ink_notes::geometry::{self, RecognizedShape};
|
use ink_notes::geometry::{self, RecognizedShape};
|
||||||
use ink_notes::image_layer::TextureSource;
|
use ink_notes::image_layer::TextureSource;
|
||||||
use ink_notes::library::Library;
|
use ink_notes::library::Library;
|
||||||
|
use ink_notes::osk::{self, KeyboardRequester};
|
||||||
use ink_notes::pdf_doc::PdfRenderService;
|
use ink_notes::pdf_doc::PdfRenderService;
|
||||||
use ink_notes::platform::{self, DisplayBackend};
|
use ink_notes::platform::{self, DisplayBackend};
|
||||||
use ink_notes::selection::{
|
use ink_notes::selection::{
|
||||||
@@ -1148,6 +1150,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bildschirmtastatur (nur Wayland): eigenes text-input-Objekt, siehe osk.rs
|
||||||
|
let mut keyboard_requester = match window.display_handle().map(|handle| handle.as_raw()) {
|
||||||
|
// Safety: `window` lebt bis zum Ende von main, die Event-Schleife besitzt beide
|
||||||
|
Ok(RawDisplayHandle::Wayland(handle)) => {
|
||||||
|
Some(unsafe { KeyboardRequester::new(handle.display.as_ptr()) })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let mut ime_was_active = false;
|
||||||
|
let mut ime_composing = false;
|
||||||
|
|
||||||
// 1. Notizbuch aus Storage laden oder initialisieren (Session-Wiederherstellung)
|
// 1. Notizbuch aus Storage laden oder initialisieren (Session-Wiederherstellung)
|
||||||
let storage_base_dir = storage::default_storage_dir();
|
let storage_base_dir = storage::default_storage_dir();
|
||||||
let _ = storage::cleanup_trash(
|
let _ = storage::cleanup_trash(
|
||||||
@@ -1307,6 +1320,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let mut pinch_prev_distance: Option<f32> = None;
|
let mut pinch_prev_distance: Option<f32> = None;
|
||||||
// Berührungen, die auf der UI begonnen haben, gehören bis zum Abheben egui
|
// Berührungen, die auf der UI begonnen haben, gehören bis zum Abheben egui
|
||||||
let mut ui_touch_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
|
let mut ui_touch_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
|
||||||
|
// Alle aufliegenden Finger (auch auf der UI), unabhängig von der Gestenauswertung
|
||||||
|
let mut touches_down: std::collections::HashSet<u64> = std::collections::HashSet::new();
|
||||||
let mut pinch_prev_center: Option<(f32, f32)> = None;
|
let mut pinch_prev_center: Option<(f32, f32)> = None;
|
||||||
let mut is_pinch_gesturing = false;
|
let mut is_pinch_gesturing = false;
|
||||||
let mut is_touch_drawing = false;
|
let mut is_touch_drawing = false;
|
||||||
@@ -1869,7 +1884,30 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
Event::WindowEvent { event, window_id } if window_id == window.id() => {
|
Event::WindowEvent { event, window_id } if window_id == window.id() => {
|
||||||
// 1. Window-Event zuerst an egui übergeben
|
// 1. Window-Event zuerst an egui übergeben
|
||||||
let egui_response = egui_winit_state.on_window_event(&window, &event);
|
if let WindowEvent::Touch(touch) = &event {
|
||||||
|
match touch.phase {
|
||||||
|
TouchPhase::Started => {
|
||||||
|
touches_down.insert(touch.id);
|
||||||
|
}
|
||||||
|
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||||
|
touches_down.remove(&touch.id);
|
||||||
|
}
|
||||||
|
TouchPhase::Moved => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Beim Antippen blendet GNOME den Mauszeiger aus und meldet CursorLeft, während der
|
||||||
|
// Finger noch aufliegt. egui-winit verwirft dabei die Zeigerposition und meldet das
|
||||||
|
// Abheben nicht mehr, der Tipp ginge verloren – daher erfährt egui davon nichts.
|
||||||
|
let egui_response = if matches!(event, WindowEvent::CursorLeft { .. })
|
||||||
|
&& !touches_down.is_empty()
|
||||||
|
{
|
||||||
|
egui_winit::EventResponse {
|
||||||
|
repaint: true,
|
||||||
|
consumed: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
egui_winit_state.on_window_event(&window, &event)
|
||||||
|
};
|
||||||
if egui_response.repaint {
|
if egui_response.repaint {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -2938,7 +2976,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. egui Eingabe erfassen und Frame starten
|
// 1. egui Eingabe erfassen und Frame starten
|
||||||
let raw_input = egui_winit_state.take_egui_input(&window);
|
let mut raw_input = egui_winit_state.take_egui_input(&window);
|
||||||
|
osk::plain_ime_commits_to_text(&mut raw_input.events, &mut ime_composing);
|
||||||
|
if let Some(requester) = keyboard_requester.as_mut() {
|
||||||
|
raw_input.events.extend(requester.take_deletions());
|
||||||
|
}
|
||||||
egui_ctx.begin_frame(raw_input);
|
egui_ctx.begin_frame(raw_input);
|
||||||
|
|
||||||
// 2. Auswahl-Overlay rendern (Bounding Box, Griffe, Lasso)
|
// 2. Auswahl-Overlay rendern (Bounding Box, Griffe, Lasso)
|
||||||
@@ -3702,7 +3744,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// 4. egui Frame beenden & Primitives tessellieren
|
// 4. egui Frame beenden & Primitives tessellieren
|
||||||
let full_output = egui_ctx.end_frame();
|
let full_output = egui_ctx.end_frame();
|
||||||
|
let ime_active = full_output.platform_output.ime.is_some();
|
||||||
egui_winit_state.handle_platform_output(&window, full_output.platform_output);
|
egui_winit_state.handle_platform_output(&window, full_output.platform_output);
|
||||||
|
if let Some(requester) = keyboard_requester.as_mut() {
|
||||||
|
let pressed = egui_ctx.input(|i| i.pointer.any_pressed());
|
||||||
|
if osk::should_request(ime_was_active, ime_active, pressed) {
|
||||||
|
requester.request();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ime_was_active = ime_active;
|
||||||
|
|
||||||
// egui braucht Folge-Frames, z. B. um neu angeordnete Bereiche zu vermessen
|
// egui braucht Folge-Frames, z. B. um neu angeordnete Bereiche zu vermessen
|
||||||
// (die Werkzeugleiste wird erst im 2.–3. Frame korrekt zentriert)
|
// (die Werkzeugleiste wird erst im 2.–3. Frame korrekt zentriert)
|
||||||
|
|||||||
+294
@@ -0,0 +1,294 @@
|
|||||||
|
//! Bildschirmtastatur unter Wayland anfordern.
|
||||||
|
//!
|
||||||
|
//! GNOME (mutter) blendet die Bildschirmtastatur nicht schon beim ersten `enable` eines
|
||||||
|
//! Texteingabefelds (`zwp_text_input_v3`) ein, sondern erst, wenn ein bereits aktives Feld
|
||||||
|
//! `enable` erneut meldet – so macht es GTK beim Antippen. winit meldet `enable` nur einmal,
|
||||||
|
//! sobald egui ein Textfeld fokussiert. Daher schickt ink-notes über ein eigenes
|
||||||
|
//! text-input-Objekt auf derselben Wayland-Verbindung ein weiteres `enable`. Die getippten
|
||||||
|
//! Zeichen kommen weiterhin über winit an.
|
||||||
|
//!
|
||||||
|
//! Die Rücktaste der GNOME-Bildschirmtastatur löscht über `delete_surrounding_text` im zuvor
|
||||||
|
//! gemeldeten Umgebungstext; ohne Umgebungstext passiert nichts, und winit wertet das Löschen
|
||||||
|
//! ohnehin nicht aus. Daher meldet das eigene Objekt einen Platzhalter als Umgebungstext und
|
||||||
|
//! übersetzt die Löschwünsche in Tastendrücke (Rücktaste/Entf) für egui.
|
||||||
|
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
use wayland_client::{
|
||||||
|
delegate_noop,
|
||||||
|
protocol::{wl_registry, wl_seat},
|
||||||
|
Connection, Dispatch, EventQueue, QueueHandle,
|
||||||
|
};
|
||||||
|
use wayland_protocols::wp::text_input::zv3::client::{
|
||||||
|
zwp_text_input_manager_v3::ZwpTextInputManagerV3,
|
||||||
|
zwp_text_input_v3::{self, ZwpTextInputV3},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Platzhalter-Umgebungstext: ein Zeichen vor der Schreibmarke, damit die Rücktaste der
|
||||||
|
/// Bildschirmtastatur immer etwas zu löschen hat (ein Byte = ein Zeichen)
|
||||||
|
const SURROUNDING_PLACEHOLDER: &str = " ";
|
||||||
|
|
||||||
|
/// Wandelt Texteingaben ohne Vorschau (Preedit) in normale Texteingaben um.
|
||||||
|
///
|
||||||
|
/// Die Bildschirmtastatur schickt jedes Zeichen als fertigen IME-Text, ohne vorher eine Vorschau
|
||||||
|
/// zu melden. egui 0.27 fügt solche Texte nur ein, solange die Schreibmarke dort steht, wo die
|
||||||
|
/// letzte Vorschau begann – also nur das erste Zeichen. `composing` merkt sich über Frames hinweg,
|
||||||
|
/// ob gerade eine Vorschau läuft; deren Abschluss bleibt bei egui.
|
||||||
|
pub fn plain_ime_commits_to_text(events: &mut [egui::Event], composing: &mut bool) {
|
||||||
|
for event in events {
|
||||||
|
match event {
|
||||||
|
egui::Event::CompositionStart => *composing = true,
|
||||||
|
egui::Event::CompositionEnd(text) => {
|
||||||
|
if !*composing && !text.is_empty() && !text.contains(['\n', '\r']) {
|
||||||
|
*event = egui::Event::Text(std::mem::take(text));
|
||||||
|
}
|
||||||
|
*composing = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tastendrücke für egui, die `back` Zeichen vor und `forward` Zeichen nach der Schreibmarke
|
||||||
|
/// löschen
|
||||||
|
pub fn deletion_key_events(back: u32, forward: u32) -> Vec<egui::Event> {
|
||||||
|
let presses = std::iter::repeat_n(egui::Key::Backspace, back as usize)
|
||||||
|
.chain(std::iter::repeat_n(egui::Key::Delete, forward as usize));
|
||||||
|
presses
|
||||||
|
.flat_map(|key| {
|
||||||
|
[true, false].map(|pressed| egui::Event::Key {
|
||||||
|
key,
|
||||||
|
physical_key: None,
|
||||||
|
pressed,
|
||||||
|
repeat: false,
|
||||||
|
modifiers: egui::Modifiers::NONE,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entscheidet, ob die Bildschirmtastatur angefordert wird: sobald ein Textfeld den Fokus
|
||||||
|
/// bekommt und erneut bei jedem Antippen des fokussierten Felds (falls die Tastatur inzwischen
|
||||||
|
/// zugeklappt wurde).
|
||||||
|
pub fn should_request(was_active: bool, active: bool, pressed: bool) -> bool {
|
||||||
|
active && (!was_active || pressed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
seat: Option<wl_seat::WlSeat>,
|
||||||
|
manager: Option<ZwpTextInputManagerV3>,
|
||||||
|
text_input: Option<ZwpTextInputV3>,
|
||||||
|
/// Von der Bildschirmtastatur gewünschtes Löschen (Zeichen vor/nach der Schreibmarke)
|
||||||
|
delete_back: u32,
|
||||||
|
delete_forward: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eigenes text-input-Objekt, mit dem ink-notes die Bildschirmtastatur anfordert
|
||||||
|
pub struct KeyboardRequester {
|
||||||
|
conn: Connection,
|
||||||
|
queue: EventQueue<State>,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardRequester {
|
||||||
|
/// Hängt sich an die Wayland-Verbindung des Fensters.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// `wl_display` muss auf die `wl_display` des Fensters zeigen und gültig bleiben, solange
|
||||||
|
/// der `KeyboardRequester` lebt.
|
||||||
|
pub unsafe fn new(wl_display: *mut c_void) -> Self {
|
||||||
|
// Safety: an den Aufrufer weitergereicht
|
||||||
|
let backend =
|
||||||
|
unsafe { wayland_backend::client::Backend::from_foreign_display(wl_display.cast()) };
|
||||||
|
let conn = Connection::from_backend(backend);
|
||||||
|
let queue = conn.new_event_queue();
|
||||||
|
conn.display().get_registry(&queue.handle(), ());
|
||||||
|
let _ = conn.flush();
|
||||||
|
Self {
|
||||||
|
conn,
|
||||||
|
queue,
|
||||||
|
state: State::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verarbeitet eingetroffene Ereignisse; einmal pro Frame aufrufen.
|
||||||
|
pub fn pump(&mut self) {
|
||||||
|
let _ = self.queue.dispatch_pending(&mut self.state);
|
||||||
|
if self.state.text_input.is_none() {
|
||||||
|
if let (Some(manager), Some(seat)) = (&self.state.manager, &self.state.seat) {
|
||||||
|
self.state.text_input =
|
||||||
|
Some(manager.get_text_input(seat, &self.queue.handle(), ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bittet den Compositor, die Bildschirmtastatur einzublenden. Muss nach dem `enable` von
|
||||||
|
/// winit kommen (also nach `handle_platform_output`).
|
||||||
|
pub fn request(&mut self) {
|
||||||
|
self.pump();
|
||||||
|
if let Some(text_input) = &self.state.text_input {
|
||||||
|
text_input.enable();
|
||||||
|
self.send_surrounding_placeholder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tastendrücke für das seit dem letzten Aufruf gewünschte Löschen (vor `begin_frame`
|
||||||
|
/// aufrufen und an die egui-Eingabe anhängen)
|
||||||
|
pub fn take_deletions(&mut self) -> Vec<egui::Event> {
|
||||||
|
self.pump();
|
||||||
|
// Den Platzhalter hier nicht neu melden: Die Tastatur löscht bei jeder neuen Meldung
|
||||||
|
// erneut, solange die Rücktaste gedrückt ist – ein kurzes Antippen löschte sonst doppelt.
|
||||||
|
let back = std::mem::take(&mut self.state.delete_back);
|
||||||
|
let forward = std::mem::take(&mut self.state.delete_forward);
|
||||||
|
deletion_key_events(back, forward)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_surrounding_placeholder(&self) {
|
||||||
|
if let Some(text_input) = &self.state.text_input {
|
||||||
|
let len = SURROUNDING_PLACEHOLDER.len() as i32;
|
||||||
|
text_input.set_surrounding_text(SURROUNDING_PLACEHOLDER.to_owned(), len, len);
|
||||||
|
text_input.commit();
|
||||||
|
let _ = self.conn.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
registry: &wl_registry::WlRegistry,
|
||||||
|
event: wl_registry::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let wl_registry::Event::Global {
|
||||||
|
name, interface, ..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
match interface.as_str() {
|
||||||
|
"wl_seat" if state.seat.is_none() => {
|
||||||
|
state.seat = Some(registry.bind(name, 1, qh, ()));
|
||||||
|
}
|
||||||
|
"zwp_text_input_manager_v3" if state.manager.is_none() => {
|
||||||
|
state.manager = Some(registry.bind(name, 1, qh, ()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate_noop!(State: ignore wl_seat::WlSeat);
|
||||||
|
delegate_noop!(State: ZwpTextInputManagerV3);
|
||||||
|
impl Dispatch<ZwpTextInputV3, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &ZwpTextInputV3,
|
||||||
|
event: zwp_text_input_v3::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
// Getippter Text kommt über winit; hier nur das Löschen
|
||||||
|
if let zwp_text_input_v3::Event::DeleteSurroundingText {
|
||||||
|
before_length,
|
||||||
|
after_length,
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
state.delete_back += before_length;
|
||||||
|
state.delete_forward += after_length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requests_when_text_field_gets_focus() {
|
||||||
|
assert!(should_request(false, true, false));
|
||||||
|
assert!(should_request(false, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requests_again_when_focused_field_is_tapped() {
|
||||||
|
assert!(should_request(true, true, true));
|
||||||
|
assert!(!should_request(true, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commits_without_preedit_become_text() {
|
||||||
|
let mut composing = false;
|
||||||
|
let mut events = vec![
|
||||||
|
egui::Event::CompositionEnd("h".into()),
|
||||||
|
egui::Event::CompositionEnd("i".into()),
|
||||||
|
];
|
||||||
|
plain_ime_commits_to_text(&mut events, &mut composing);
|
||||||
|
assert_eq!(
|
||||||
|
events,
|
||||||
|
vec![egui::Event::Text("h".into()), egui::Event::Text("i".into())]
|
||||||
|
);
|
||||||
|
assert!(!composing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commits_after_preedit_stay_with_egui() {
|
||||||
|
let mut composing = false;
|
||||||
|
let mut events = vec![
|
||||||
|
egui::Event::CompositionStart,
|
||||||
|
egui::Event::CompositionUpdate("ni".into()),
|
||||||
|
];
|
||||||
|
plain_ime_commits_to_text(&mut events, &mut composing);
|
||||||
|
assert!(composing);
|
||||||
|
// Abschluss erst im nächsten Frame
|
||||||
|
let mut events = vec![egui::Event::CompositionEnd("你".into())];
|
||||||
|
plain_ime_commits_to_text(&mut events, &mut composing);
|
||||||
|
assert_eq!(events, vec![egui::Event::CompositionEnd("你".into())]);
|
||||||
|
assert!(!composing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_breaks_and_empty_commits_are_left_alone() {
|
||||||
|
let mut composing = false;
|
||||||
|
let mut events = vec![
|
||||||
|
egui::Event::CompositionEnd("\n".into()),
|
||||||
|
egui::Event::CompositionEnd(String::new()),
|
||||||
|
];
|
||||||
|
let before = events.clone();
|
||||||
|
plain_ime_commits_to_text(&mut events, &mut composing);
|
||||||
|
assert_eq!(events, before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deletions_become_key_presses() {
|
||||||
|
let events = deletion_key_events(2, 1);
|
||||||
|
let keys: Vec<_> = events
|
||||||
|
.iter()
|
||||||
|
.map(|e| match e {
|
||||||
|
egui::Event::Key { key, pressed, .. } => (*key, *pressed),
|
||||||
|
other => panic!("unerwartet: {other:?}"),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
keys,
|
||||||
|
vec![
|
||||||
|
(egui::Key::Backspace, true),
|
||||||
|
(egui::Key::Backspace, false),
|
||||||
|
(egui::Key::Backspace, true),
|
||||||
|
(egui::Key::Backspace, false),
|
||||||
|
(egui::Key::Delete, true),
|
||||||
|
(egui::Key::Delete, false),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(deletion_key_events(0, 0).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_request_without_focused_text_field() {
|
||||||
|
assert!(!should_request(false, false, true));
|
||||||
|
assert!(!should_request(true, false, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user