From fec49fedb6b0af8c8bcd9dee567d9188f6d0d385 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 24 Apr 2026 14:33:00 +0000 Subject: [PATCH 01/10] Move edit_x_coord to Common --- crates/kas-widgets/src/edit/editor.rs | 29 +++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 13d68662a..d21ffe131 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -74,6 +74,7 @@ pub struct Common { direction: Direction, wrap: bool, read_only: bool, + edit_x_coord: Option, } impl Common { @@ -87,6 +88,7 @@ impl Common { direction: Direction::Auto, wrap, read_only: false, + edit_x_coord: None, } } @@ -140,7 +142,6 @@ pub struct Part { highlight: highlight::Cache, text: String, selection: CursorRange, - edit_x_coord: Option, last_edit: Option, undo_stack: UndoStack<(String, CursorRange)>, has_key_focus: bool, @@ -367,7 +368,6 @@ impl Default for Part { highlight: Default::default(), text: Default::default(), selection: Default::default(), - edit_x_coord: None, last_edit: Some(EditOp::Initial), undo_stack: UndoStack::new(), has_key_focus: false, @@ -825,7 +825,7 @@ impl Part { let selection = self.selection.to_range(); self.replace_range(selection.clone(), text); self.selection.set_position(selection.start + text.len()); - self.edit_x_coord = None; + common.edit_x_coord = None; EventAction::Edit } else { @@ -898,7 +898,7 @@ impl Part { self.current = CurrentAction::ImePreedit { edit_range: edit_range.cast(), }; - self.edit_x_coord = None; + common.edit_x_coord = None; return EventAction::Preedit; } Ime::Commit { text } => { @@ -915,7 +915,7 @@ impl Part { self.current = CurrentAction::ImePreedit { edit_range: self.selection.to_range().cast(), }; - self.edit_x_coord = None; + common.edit_x_coord = None; return EventAction::Edit; } Ime::DeleteSurrounding { @@ -1052,7 +1052,7 @@ impl Part { if range != self.selection { self.selection = range; self.set_view_offset_from_cursor(cx); - self.edit_x_coord = None; + common.edit_x_coord = None; cx.redraw(); } event_action @@ -1338,7 +1338,7 @@ impl Part { // Avoid use of unused navigation keys (e.g. by ScrollComponent): Command::Left | Command::Right | Command::WordLeft | Command::WordRight => Action::None, Command::Up | Command::Down if multi_line => { - let x = match self.edit_x_coord { + let x = match common.edit_x_coord { Some(x) => x, None => self .display @@ -1391,7 +1391,7 @@ impl Part { .next_back() .map(|r| r.pos.into()) .unwrap_or(Vec2::ZERO); - if let Some(x) = self.edit_x_coord { + if let Some(x) = common.edit_x_coord { v.0 = x; } // TODO: page height should be an input? @@ -1497,13 +1497,13 @@ impl Part { }; self.replace_range(range, s); self.selection.set_position(index + s.len()); - self.edit_x_coord = None; + common.edit_x_coord = None; EventAction::Edit } Action::Delete(sel, _) => { self.replace_range(sel.clone(), ""); self.selection.set_position(sel.start); - self.edit_x_coord = None; + common.edit_x_coord = None; EventAction::Edit } Action::Move(index, x_coord) => { @@ -1513,7 +1513,7 @@ impl Part { } else { self.set_primary(cx); } - self.edit_x_coord = x_coord; + common.edit_x_coord = x_coord; cx.redraw(); EventAction::Cursor } @@ -1522,7 +1522,7 @@ impl Part { if self.text.as_str() != text { self.text = text.clone(); self.status = Status::New; - self.edit_x_coord = None; + common.edit_x_coord = None; } self.selection = *cursor; EventAction::Edit @@ -1645,7 +1645,7 @@ impl Editor { let len = self.as_str().len(); self.part.selection.set_max_len(len); - self.part.edit_x_coord = None; + self.common.edit_x_coord = None; self.error_state = None; } @@ -1662,7 +1662,6 @@ impl Editor { self.part .selection .set_position(selection.start + text.len()); - self.part.edit_x_coord = None; self.error_state = None; } @@ -1678,7 +1677,7 @@ impl Editor { /// guard. #[inline] pub fn set_cursor_range(&mut self, range: CursorRange) { - self.part.edit_x_coord = None; + self.common.edit_x_coord = None; self.part.selection = range; } From 7c1a8afaa94d745403ed2ce23d0bfe08d3e2212f Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 25 Apr 2026 14:55:28 +0000 Subject: [PATCH 02/10] Move selection to Common Note: translations will be needed to support multiple text parts --- crates/kas-widgets/src/edit/editor.rs | 177 +++++++++++++------------- 1 file changed, 86 insertions(+), 91 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index d21ffe131..8e26d9547 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -75,6 +75,7 @@ pub struct Common { wrap: bool, read_only: bool, edit_x_coord: Option, + selection: CursorRange, } impl Common { @@ -89,6 +90,7 @@ impl Common { wrap, read_only: false, edit_x_coord: None, + selection: CursorRange::default(), } } @@ -141,7 +143,6 @@ pub struct Part { display: TextDisplay, highlight: highlight::Cache, text: String, - selection: CursorRange, last_edit: Option, undo_stack: UndoStack<(String, CursorRange)>, has_key_focus: bool, @@ -264,7 +265,7 @@ impl Component { pub fn with_text(mut self, text: impl ToString) -> Self { self.0.part = self.0.part.with_text(text); let index = if self.0.common.wrap { 0 } else { self.0.part.text.len() }; - self.0.part.selection.set_position(index); + self.0.common.selection.set_position(index); self } @@ -367,7 +368,6 @@ impl Default for Part { display: TextDisplay::default(), highlight: Default::default(), text: Default::default(), - selection: Default::default(), last_edit: Some(EditOp::Initial), undo_stack: UndoStack::new(), has_key_focus: false, @@ -405,12 +405,6 @@ impl Part { self.display.text_is_rtl() } - /// Access the cursor index / selection range - #[inline] - pub fn cursor_range(&self) -> CursorRange { - self.selection - } - /// Check whether the text is fully prepared and ready for usage #[inline] pub fn is_prepared(&self) -> bool { @@ -523,7 +517,7 @@ impl Part { self.prepare_wrap(common); if self.current.is_ime_enabled() { - self.set_ime_cursor_area(cx); + self.set_ime_cursor_area(common, cx); } } @@ -583,7 +577,7 @@ impl Part { self.prepare_runs(common, highlighter); if self.prepare_wrap(common) { cx.resize(); - self.set_view_offset_from_cursor(cx); + self.set_view_offset_from_cursor(common, cx); } cx.redraw(); } @@ -622,7 +616,7 @@ impl Part { } let pos = self.rect.pos - offset; - let range: Range = self.selection.to_range().cast(); + let range: Range = common.selection.to_range().cast(); let color_tokens = self.highlight.color_tokens(); let default_colors = format::Colors { @@ -731,7 +725,7 @@ impl Part { pos, rect, &self.display, - self.selection.cursor, + common.selection.cursor, Some(common.colors.cursor), ); } @@ -768,19 +762,19 @@ impl Part { // the latter. We must set before calling self.set_primary. self.has_key_focus = true; if source == FocusSource::Pointer { - self.set_primary(cx); + self.set_primary(common, cx); } return EventAction::Used; } Event::KeyFocus => { self.has_key_focus = true; - self.set_view_offset_from_cursor(cx); + self.set_view_offset_from_cursor(common, cx); return if self.current.is_none() { let hint = Default::default(); let purpose = ImePurpose::Normal; - let surrounding_text = self.ime_surrounding_text(); + let surrounding_text = self.ime_surrounding_text(common); cx.replace_ime_focus(self.id.clone(), hint, purpose, surrounding_text); EventAction::FocusGained } else { @@ -798,9 +792,9 @@ impl Part { } Event::LostSelFocus => { // NOTE: we can assume that we will receive Ime::Disabled if IME is active - if !self.selection.is_empty() { - self.save_undo_state(None); - self.selection.clear_selection(); + if !common.selection.is_empty() { + self.save_undo_state(common, None); + common.selection.clear_selection(); } self.input_handler.stop_selecting(); cx.redraw(); @@ -809,7 +803,7 @@ impl Part { Event::Command(cmd, code) => match self.cmd_action(common, cx, cmd, code) { Ok(action) => { if matches!(action, EventAction::Cursor) { - self.set_view_offset_from_cursor(cx); + self.set_view_offset_from_cursor(common, cx); } return action; } @@ -819,12 +813,12 @@ impl Part { if event.state == ElementState::Pressed && !common.read_only => { return if let Some(text) = &event.text { - self.save_undo_state(Some(EditOp::KeyInput)); - self.cancel_selection_and_ime(cx); + self.save_undo_state(common, Some(EditOp::KeyInput)); + self.cancel_selection_and_ime(common, cx); - let selection = self.selection.to_range(); + let selection = common.selection.to_range(); self.replace_range(selection.clone(), text); - self.selection.set_position(selection.start + text.len()); + common.selection.set_position(selection.start + text.len()); common.edit_x_coord = None; EventAction::Edit @@ -837,7 +831,7 @@ impl Part { match self.cmd_action(common, cx, cmd, Some(event.physical_key)) { Ok(action) => { if matches!(action, EventAction::Cursor) { - self.set_view_offset_from_cursor(cx); + self.set_view_offset_from_cursor(common, cx); } action } @@ -853,7 +847,7 @@ impl Part { match self.current { CurrentAction::None => { self.current = CurrentAction::ImeStart; - self.set_ime_cursor_area(cx); + self.set_ime_cursor_area(common, cx); } CurrentAction::ImeStart | CurrentAction::ImePreedit { .. } => { // already enabled @@ -870,7 +864,7 @@ impl Part { }; } Ime::Disabled => { - self.clear_ime(); + self.clear_ime(common); return if !self.has_key_focus { EventAction::FocusLost } else { @@ -878,9 +872,9 @@ impl Part { }; } Ime::Preedit { text, cursor } => { - self.save_undo_state(None); + self.save_undo_state(common, None); let mut edit_range = match self.current.clone() { - CurrentAction::ImeStart if cursor.is_some() => self.selection.to_range(), + CurrentAction::ImeStart if cursor.is_some() => common.selection.to_range(), CurrentAction::ImeStart => return EventAction::Used, CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, @@ -889,10 +883,10 @@ impl Part { self.replace_range(edit_range.clone(), text); edit_range.end = edit_range.start + text.len(); if let Some((start, end)) = cursor { - self.selection.anchor = edit_range.start + start; - self.selection.cursor = edit_range.start + end; + common.selection.anchor = edit_range.start + start; + common.selection.cursor = edit_range.start + end; } else { - self.selection.set_position(edit_range.start + text.len()); + common.selection.set_position(edit_range.start + text.len()); } self.current = CurrentAction::ImePreedit { @@ -902,18 +896,18 @@ impl Part { return EventAction::Preedit; } Ime::Commit { text } => { - self.save_undo_state(Some(EditOp::Ime)); + self.save_undo_state(common, Some(EditOp::Ime)); let edit_range = match self.current.clone() { - CurrentAction::ImeStart => self.selection.to_range(), + CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, }; self.replace_range(edit_range.clone(), text); - self.selection.set_position(edit_range.start + text.len()); + common.selection.set_position(edit_range.start + text.len()); self.current = CurrentAction::ImePreedit { - edit_range: self.selection.to_range().cast(), + edit_range: common.selection.to_range().cast(), }; common.edit_x_coord = None; return EventAction::Edit; @@ -922,9 +916,9 @@ impl Part { before_bytes, after_bytes, } => { - self.save_undo_state(None); + self.save_undo_state(common, None); let edit_range = match self.current.clone() { - CurrentAction::ImeStart => self.selection.to_range(), + CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, }; @@ -934,7 +928,7 @@ impl Part { let start = end - before_bytes; if self.as_str().is_char_boundary(start) { self.replace_range(start..end, ""); - self.selection.delete_range(start..end); + common.selection.delete_range(start..end); } else { log::warn!("buggy IME tried to delete range not at char boundary"); } @@ -950,7 +944,7 @@ impl Part { } } - if let Some(text) = self.ime_surrounding_text() { + if let Some(text) = self.ime_surrounding_text(common) { cx.update_ime_surrounding_text(&self.id, text); } @@ -966,11 +960,11 @@ impl Part { Event::PressEnd { press, .. } if press.is_tertiary() => { let rel_pos = (press.coord - self.rect().pos).cast(); let mut index = self.display.text_index_nearest(rel_pos); - self.cancel_selection_and_ime(cx); + self.cancel_selection_and_ime(common, cx); self.request_key_focus(cx, FocusSource::Pointer); if let Some(content) = cx.get_primary() { - self.save_undo_state(Some(EditOp::Clipboard)); + self.save_undo_state(common, Some(EditOp::Clipboard)); let range = self.trim_paste(common.wrap, &content); self.replace_range(index..index, &content[range.clone()]); @@ -988,16 +982,16 @@ impl Part { repeats, } => { if self.current.is_ime_enabled() { - self.clear_ime(); + self.clear_ime(common); cx.cancel_ime_focus(&self.id); } self.request_key_focus(cx, FocusSource::Pointer); - self.save_undo_state(Some(EditOp::Cursor)); + self.save_undo_state(common, Some(EditOp::Cursor)); self.current = CurrentAction::Selection; let rel_pos = (coord - self.rect().pos).cast(); let cursor = self.display.text_index_nearest(rel_pos); - let anchor = if clear { cursor } else { self.selection.anchor }; + let anchor = if clear { cursor } else { common.selection.anchor }; let range = CursorRange::from(anchor..cursor); if repeats > 1 { @@ -1021,7 +1015,7 @@ impl Part { TextInput::adjust_range( self.text.as_str(), - self.selection, + common.selection, index, repeats, Some(&|index| self.display.find_line(index).map(|r| r.1)), @@ -1029,17 +1023,17 @@ impl Part { } TextInputAction::PressEnd { coord } => { if self.current.is_ime_enabled() { - self.clear_ime(); + self.clear_ime(common); cx.cancel_ime_focus(&self.id); } - self.save_undo_state(Some(EditOp::Cursor)); + self.save_undo_state(common, Some(EditOp::Cursor)); if self.current == CurrentAction::Selection { - self.set_primary(cx); + self.set_primary(common, cx); } else { let rel_pos = (coord - self.rect().pos).cast(); let index = self.display.text_index_nearest(rel_pos); - self.selection.cursor = index; - self.selection.clear_selection(); + common.selection.cursor = index; + common.selection.clear_selection(); } self.current = CurrentAction::None; @@ -1049,9 +1043,9 @@ impl Part { }, }; - if range != self.selection { - self.selection = range; - self.set_view_offset_from_cursor(cx); + if range != common.selection { + common.selection = range; + self.set_view_offset_from_cursor(common, cx); common.edit_x_coord = None; cx.redraw(); } @@ -1077,12 +1071,12 @@ impl Part { /// /// This should be called if e.g. key-input interrupts the current /// action. - fn cancel_selection_and_ime(&mut self, cx: &mut EventState) { + fn cancel_selection_and_ime(&mut self, common: &mut Common, cx: &mut EventState) { if self.current == CurrentAction::Selection { self.input_handler.stop_selecting(); self.current = CurrentAction::None; } else if self.current.is_ime_enabled() { - self.clear_ime(); + self.clear_ime(common); cx.cancel_ime_focus(&self.id); } } @@ -1091,20 +1085,20 @@ impl Part { /// /// One should also call [`EventCx::cancel_ime_focus`] unless this is /// implied. - fn clear_ime(&mut self) { + fn clear_ime(&mut self, common: &mut Common) { if self.current.is_ime_enabled() { let action = std::mem::replace(&mut self.current, CurrentAction::None); if let CurrentAction::ImePreedit { edit_range } = action { - self.selection.set_position(edit_range.start.cast()); + common.selection.set_position(edit_range.start.cast()); self.replace_range(edit_range.cast(), ""); } } } - fn ime_surrounding_text(&self) -> Option { + fn ime_surrounding_text(&self, common: &Common) -> Option { const MAX_TEXT_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES; - let sel_range = self.selection.to_range(); + let sel_range = common.selection.to_range(); let edit_range = match self.current.clone() { CurrentAction::ImePreedit { edit_range } => Some(edit_range.cast()), _ => None, @@ -1145,9 +1139,9 @@ impl Part { text = self.as_str()[range].to_string(); } - let cursor = self.selection.cursor.saturating_sub(start); + let cursor = common.selection.cursor.saturating_sub(start); // Terminology difference: our sel_index is called 'anchor' - let sel_index = self.selection.anchor.saturating_sub(start); + let sel_index = common.selection.anchor.saturating_sub(start); ImeSurroundingText::new(text, cursor, sel_index) .inspect_err(|err| { // TODO: use Display for err not Debug @@ -1157,13 +1151,13 @@ impl Part { } /// Call to set IME position only while IME is active - fn set_ime_cursor_area(&self, cx: &mut EventState) { + fn set_ime_cursor_area(&self, common: &Common, cx: &mut EventState) { if !self.is_prepared() { return; } let range = match self.current.clone() { - CurrentAction::ImeStart => self.selection.to_range(), + CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return, }; @@ -1200,7 +1194,7 @@ impl Part { /// Call before an edit to (potentially) commit current state based on last_edit /// /// Call with [`None`] to force commit of any uncommitted changes. - fn save_undo_state(&mut self, edit: Option) { + fn save_undo_state(&mut self, common: &mut Common, edit: Option) { if let Some(op) = edit && op.try_merge(&mut self.last_edit) { @@ -1209,7 +1203,7 @@ impl Part { self.last_edit = edit; self.undo_stack - .try_push((self.as_str().to_string(), self.selection)); + .try_push((self.as_str().to_string(), common.selection)); } /// Request key focus, if we don't have it or IME @@ -1248,10 +1242,10 @@ impl Part { let editable = !common.read_only; let mut shift = cx.modifiers().shift_key(); let mut buf = [0u8; 4]; - let cursor = self.selection.cursor; + let cursor = common.selection.cursor; let len = self.as_str().len(); let multi_line = common.wrap; - let selection = self.selection.to_range(); + let selection = common.selection.to_range(); let have_sel = selection.end > selection.start; let string; @@ -1436,7 +1430,7 @@ impl Part { Action::Delete(prev..cursor, EditOp::Delete) } Command::SelectAll => { - self.selection.anchor = 0; + common.selection.anchor = 0; shift = true; // hack Action::Move(len, None) } @@ -1468,7 +1462,7 @@ impl Part { } if !matches!(action, Action::None) { - self.cancel_selection_and_ime(cx); + self.cancel_selection_and_ime(common, cx); } let edit_op = match action { @@ -1477,12 +1471,12 @@ impl Part { Action::Activate | Action::UndoRedo(_) => None, Action::Insert(_, edit) | Action::Delete(_, edit) => Some(edit), }; - self.save_undo_state(edit_op); + self.save_undo_state(common, edit_op); let action = match action { Action::None => unreachable!(), Action::Deselect => { - self.selection.clear_selection(); + common.selection.clear_selection(); cx.redraw(); EventAction::Cursor } @@ -1496,22 +1490,22 @@ impl Part { index..index }; self.replace_range(range, s); - self.selection.set_position(index + s.len()); + common.selection.set_position(index + s.len()); common.edit_x_coord = None; EventAction::Edit } Action::Delete(sel, _) => { self.replace_range(sel.clone(), ""); - self.selection.set_position(sel.start); + common.selection.set_position(sel.start); common.edit_x_coord = None; EventAction::Edit } Action::Move(index, x_coord) => { - self.selection.cursor = index; + common.selection.cursor = index; if !shift { - self.selection.clear_selection(); + common.selection.clear_selection(); } else { - self.set_primary(cx); + self.set_primary(common, cx); } common.edit_x_coord = x_coord; cx.redraw(); @@ -1524,7 +1518,7 @@ impl Part { self.status = Status::New; common.edit_x_coord = None; } - self.selection = *cursor; + common.selection = *cursor; EventAction::Edit } else { EventAction::Used @@ -1536,9 +1530,9 @@ impl Part { } /// Set primary clipboard (mouse buffer) contents from selection - fn set_primary(&self, cx: &mut EventCx) { - if self.has_key_focus && !self.selection.is_empty() && cx.has_primary() { - let range = self.selection.to_range(); + fn set_primary(&self, common: &Common, cx: &mut EventCx) { + if self.has_key_focus && !common.selection.is_empty() && cx.has_primary() { + let range = common.selection.to_range(); cx.set_primary(String::from(&self.as_str()[range])); } } @@ -1548,8 +1542,8 @@ impl Part { /// It is assumed that the text has not changed. /// /// A redraw is assumed since the cursor moved. - fn set_view_offset_from_cursor(&mut self, cx: &mut EventCx) { - let cursor = self.selection.cursor; + fn set_view_offset_from_cursor(&mut self, common: &Common, cx: &mut EventCx) { + let cursor = common.selection.cursor; if self.is_prepared() && let Some(marker) = self.display.text_glyph_pos(cursor).next_back() { @@ -1602,7 +1596,8 @@ impl Editor { /// [`Self::set_string`] to commit changes to the undo history. #[inline] pub fn pre_commit(&mut self) { - self.part.save_undo_state(Some(EditOp::Synthetic)); + self.part + .save_undo_state(&mut self.common, Some(EditOp::Synthetic)); } /// Clear text contents and undo history @@ -1638,13 +1633,13 @@ impl Editor { return; // no change } - self.part.cancel_selection_and_ime(cx); + self.part.cancel_selection_and_ime(&mut self.common, cx); self.part.text = text; self.part.require_reprepare(); let len = self.as_str().len(); - self.part.selection.set_max_len(len); + self.common.selection.set_max_len(len); self.common.edit_x_coord = None; self.error_state = None; } @@ -1655,11 +1650,11 @@ impl Editor { /// guard. #[inline] pub fn replace_selected_text(&mut self, cx: &mut EventState, text: &str) { - self.part.cancel_selection_and_ime(cx); + self.part.cancel_selection_and_ime(&mut self.common, cx); - let selection = self.part.selection.to_range(); + let selection = self.common.selection.to_range(); self.part.replace_range(selection.clone(), text); - self.part + self.common .selection .set_position(selection.start + text.len()); self.error_state = None; @@ -1668,7 +1663,7 @@ impl Editor { /// Access the cursor index / selection range #[inline] pub fn cursor_range(&self) -> CursorRange { - self.part.selection + self.common.selection } /// Set the cursor index / range @@ -1678,7 +1673,7 @@ impl Editor { #[inline] pub fn set_cursor_range(&mut self, range: CursorRange) { self.common.edit_x_coord = None; - self.part.selection = range; + self.common.selection = range; } /// Get whether this text-edit widget is read-only From b25b75f242206e83521caafb392a98f4aae9fe95 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 25 Apr 2026 15:40:49 +0000 Subject: [PATCH 03/10] Move last_edit, undo_stack to Common --- crates/kas-widgets/src/edit/editor.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 8e26d9547..a690735bc 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -76,6 +76,8 @@ pub struct Common { read_only: bool, edit_x_coord: Option, selection: CursorRange, + last_edit: Option, + undo_stack: UndoStack<(String, CursorRange)>, } impl Common { @@ -91,6 +93,8 @@ impl Common { read_only: false, edit_x_coord: None, selection: CursorRange::default(), + last_edit: Some(EditOp::Initial), + undo_stack: UndoStack::new(), } } @@ -143,8 +147,6 @@ pub struct Part { display: TextDisplay, highlight: highlight::Cache, text: String, - last_edit: Option, - undo_stack: UndoStack<(String, CursorRange)>, has_key_focus: bool, current: CurrentAction, input_handler: TextInput, @@ -368,8 +370,6 @@ impl Default for Part { display: TextDisplay::default(), highlight: Default::default(), text: Default::default(), - last_edit: Some(EditOp::Initial), - undo_stack: UndoStack::new(), has_key_focus: false, current: CurrentAction::None, input_handler: Default::default(), @@ -1196,13 +1196,14 @@ impl Part { /// Call with [`None`] to force commit of any uncommitted changes. fn save_undo_state(&mut self, common: &mut Common, edit: Option) { if let Some(op) = edit - && op.try_merge(&mut self.last_edit) + && op.try_merge(&mut common.last_edit) { return; } - self.last_edit = edit; - self.undo_stack + common.last_edit = edit; + common + .undo_stack .try_push((self.as_str().to_string(), common.selection)); } @@ -1512,7 +1513,7 @@ impl Part { EventAction::Cursor } Action::UndoRedo(redo) => { - if let Some((text, cursor)) = self.undo_stack.undo_or_redo(redo) { + if let Some((text, cursor)) = common.undo_stack.undo_or_redo(redo) { if self.text.as_str() != text { self.text = text.clone(); self.status = Status::New; @@ -1603,8 +1604,8 @@ impl Editor { /// Clear text contents and undo history #[inline] pub fn clear(&mut self, cx: &mut EventState) { - self.part.last_edit = Some(EditOp::Initial); - self.part.undo_stack.clear(); + self.common.last_edit = Some(EditOp::Initial); + self.common.undo_stack.clear(); self.set_string(cx, String::new()); } From 703ee01f64aba1f31f1a13674a070536b68c26d7 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Sat, 25 Apr 2026 15:37:37 +0000 Subject: [PATCH 04/10] Move CurrentAction, input_handler to Common --- crates/kas-widgets/src/edit/editor.rs | 86 ++++++++++++++------------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index a690735bc..05687e043 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -78,6 +78,8 @@ pub struct Common { selection: CursorRange, last_edit: Option, undo_stack: UndoStack<(String, CursorRange)>, + current: CurrentAction, + input_handler: TextInput, } impl Common { @@ -95,6 +97,8 @@ impl Common { selection: CursorRange::default(), last_edit: Some(EditOp::Initial), undo_stack: UndoStack::new(), + current: CurrentAction::None, + input_handler: Default::default(), } } @@ -148,8 +152,6 @@ pub struct Part { highlight: highlight::Cache, text: String, has_key_focus: bool, - current: CurrentAction, - input_handler: TextInput, } /// Inner editor interface @@ -265,6 +267,11 @@ impl Component { #[inline] #[must_use] pub fn with_text(mut self, text: impl ToString) -> Self { + debug_assert!( + self.0.common.current == CurrentAction::None + && !self.0.common.input_handler.is_selecting() + ); + self.0.part = self.0.part.with_text(text); let index = if self.0.common.wrap { 0 } else { self.0.part.text.len() }; self.0.common.selection.set_position(index); @@ -371,8 +378,6 @@ impl Default for Part { highlight: Default::default(), text: Default::default(), has_key_focus: false, - current: CurrentAction::None, - input_handler: Default::default(), } } } @@ -384,7 +389,6 @@ impl Part { #[inline] #[must_use] pub fn with_text(mut self, text: impl ToString) -> Self { - debug_assert!(self.current == CurrentAction::None && !self.input_handler.is_selecting()); let text = text.to_string(); self.text = text; self @@ -516,7 +520,7 @@ impl Part { self.rect = rect; self.prepare_wrap(common); - if self.current.is_ime_enabled() { + if common.current.is_ime_enabled() { self.set_ime_cursor_area(common, cx); } } @@ -707,7 +711,7 @@ impl Part { draw.decorate_text(pos, rect, &self.display, decorations); } - if let CurrentAction::ImePreedit { edit_range } = self.current.clone() { + if let CurrentAction::ImePreedit { edit_range } = common.current.clone() { let tokens = [ Default::default(), (edit_range.start, format::Decoration { @@ -750,8 +754,8 @@ impl Part { let mut event_action = EventAction::Used; let range = match event { Event::NavFocus(source) if source == FocusSource::Key => { - if !self.input_handler.is_selecting() { - self.request_key_focus(cx, source); + if !common.input_handler.is_selecting() { + self.request_key_focus(common, cx, source); } return EventAction::Used; } @@ -771,7 +775,7 @@ impl Part { self.has_key_focus = true; self.set_view_offset_from_cursor(common, cx); - return if self.current.is_none() { + return if common.current.is_none() { let hint = Default::default(); let purpose = ImePurpose::Normal; let surrounding_text = self.ime_surrounding_text(common); @@ -784,7 +788,7 @@ impl Part { Event::LostKeyFocus => { self.has_key_focus = false; cx.redraw(); - return if !self.current.is_ime_enabled() { + return if !common.current.is_ime_enabled() { EventAction::FocusLost } else { EventAction::Used @@ -796,7 +800,7 @@ impl Part { self.save_undo_state(common, None); common.selection.clear_selection(); } - self.input_handler.stop_selecting(); + common.input_handler.stop_selecting(); cx.redraw(); return EventAction::Used; } @@ -844,9 +848,9 @@ impl Part { } Event::Ime(ime) => match ime { Ime::Enabled => { - match self.current { + match common.current { CurrentAction::None => { - self.current = CurrentAction::ImeStart; + common.current = CurrentAction::ImeStart; self.set_ime_cursor_area(common, cx); } CurrentAction::ImeStart | CurrentAction::ImePreedit { .. } => { @@ -873,7 +877,7 @@ impl Part { } Ime::Preedit { text, cursor } => { self.save_undo_state(common, None); - let mut edit_range = match self.current.clone() { + let mut edit_range = match common.current.clone() { CurrentAction::ImeStart if cursor.is_some() => common.selection.to_range(), CurrentAction::ImeStart => return EventAction::Used, CurrentAction::ImePreedit { edit_range } => edit_range.cast(), @@ -889,7 +893,7 @@ impl Part { common.selection.set_position(edit_range.start + text.len()); } - self.current = CurrentAction::ImePreedit { + common.current = CurrentAction::ImePreedit { edit_range: edit_range.cast(), }; common.edit_x_coord = None; @@ -897,7 +901,7 @@ impl Part { } Ime::Commit { text } => { self.save_undo_state(common, Some(EditOp::Ime)); - let edit_range = match self.current.clone() { + let edit_range = match common.current.clone() { CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, @@ -906,7 +910,7 @@ impl Part { self.replace_range(edit_range.clone(), text); common.selection.set_position(edit_range.start + text.len()); - self.current = CurrentAction::ImePreedit { + common.current = CurrentAction::ImePreedit { edit_range: common.selection.to_range().cast(), }; common.edit_x_coord = None; @@ -917,7 +921,7 @@ impl Part { after_bytes, } => { self.save_undo_state(common, None); - let edit_range = match self.current.clone() { + let edit_range = match common.current.clone() { CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return EventAction::Used, @@ -961,7 +965,7 @@ impl Part { let rel_pos = (press.coord - self.rect().pos).cast(); let mut index = self.display.text_index_nearest(rel_pos); self.cancel_selection_and_ime(common, cx); - self.request_key_focus(cx, FocusSource::Pointer); + self.request_key_focus(common, cx, FocusSource::Pointer); if let Some(content) = cx.get_primary() { self.save_undo_state(common, Some(EditOp::Clipboard)); @@ -973,7 +977,7 @@ impl Part { } index.into() } - event => match self.input_handler.handle(cx, self.id.clone(), event) { + event => match common.input_handler.handle(cx, self.id.clone(), event) { TextInputAction::Used => return EventAction::Used, TextInputAction::Unused => return EventAction::Unused, TextInputAction::PressStart { @@ -981,13 +985,13 @@ impl Part { clear, repeats, } => { - if self.current.is_ime_enabled() { + if common.current.is_ime_enabled() { self.clear_ime(common); cx.cancel_ime_focus(&self.id); } - self.request_key_focus(cx, FocusSource::Pointer); + self.request_key_focus(common, cx, FocusSource::Pointer); self.save_undo_state(common, Some(EditOp::Cursor)); - self.current = CurrentAction::Selection; + common.current = CurrentAction::Selection; let rel_pos = (coord - self.rect().pos).cast(); let cursor = self.display.text_index_nearest(rel_pos); @@ -1006,7 +1010,7 @@ impl Part { } } TextInputAction::PressMove { coord, repeats } => { - if self.current != CurrentAction::Selection { + if common.current != CurrentAction::Selection { return EventAction::Used; } @@ -1022,12 +1026,12 @@ impl Part { ) } TextInputAction::PressEnd { coord } => { - if self.current.is_ime_enabled() { + if common.current.is_ime_enabled() { self.clear_ime(common); cx.cancel_ime_focus(&self.id); } self.save_undo_state(common, Some(EditOp::Cursor)); - if self.current == CurrentAction::Selection { + if common.current == CurrentAction::Selection { self.set_primary(common, cx); } else { let rel_pos = (coord - self.rect().pos).cast(); @@ -1035,9 +1039,9 @@ impl Part { common.selection.cursor = index; common.selection.clear_selection(); } - self.current = CurrentAction::None; + common.current = CurrentAction::None; - self.request_key_focus(cx, FocusSource::Pointer); + self.request_key_focus(common, cx, FocusSource::Pointer); return EventAction::Used; } }, @@ -1072,10 +1076,10 @@ impl Part { /// This should be called if e.g. key-input interrupts the current /// action. fn cancel_selection_and_ime(&mut self, common: &mut Common, cx: &mut EventState) { - if self.current == CurrentAction::Selection { - self.input_handler.stop_selecting(); - self.current = CurrentAction::None; - } else if self.current.is_ime_enabled() { + if common.current == CurrentAction::Selection { + common.input_handler.stop_selecting(); + common.current = CurrentAction::None; + } else if common.current.is_ime_enabled() { self.clear_ime(common); cx.cancel_ime_focus(&self.id); } @@ -1086,8 +1090,8 @@ impl Part { /// One should also call [`EventCx::cancel_ime_focus`] unless this is /// implied. fn clear_ime(&mut self, common: &mut Common) { - if self.current.is_ime_enabled() { - let action = std::mem::replace(&mut self.current, CurrentAction::None); + if common.current.is_ime_enabled() { + let action = std::mem::replace(&mut common.current, CurrentAction::None); if let CurrentAction::ImePreedit { edit_range } = action { common.selection.set_position(edit_range.start.cast()); self.replace_range(edit_range.cast(), ""); @@ -1099,7 +1103,7 @@ impl Part { const MAX_TEXT_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES; let sel_range = common.selection.to_range(); - let edit_range = match self.current.clone() { + let edit_range = match common.current.clone() { CurrentAction::ImePreedit { edit_range } => Some(edit_range.cast()), _ => None, }; @@ -1156,7 +1160,7 @@ impl Part { return; } - let range = match self.current.clone() { + let range = match common.current.clone() { CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), _ => return, @@ -1208,8 +1212,8 @@ impl Part { } /// Request key focus, if we don't have it or IME - fn request_key_focus(&self, cx: &mut EventCx, source: FocusSource) { - if !self.has_key_focus && !self.current.is_ime_enabled() { + fn request_key_focus(&self, common: &Common, cx: &mut EventCx, source: FocusSource) { + if !self.has_key_focus && !common.current.is_ime_enabled() { cx.request_key_focus(self.id.clone(), source); } } @@ -1459,7 +1463,7 @@ impl Part { // We can receive some commands without key focus as a result of // selection focus. Request focus on edit actions (like Command::Cut). if !matches!(action, Action::None | Action::Deselect) { - self.request_key_focus(cx, FocusSource::Synthetic); + self.request_key_focus(common, cx, FocusSource::Synthetic); } if !matches!(action, Action::None) { @@ -1700,7 +1704,7 @@ impl Editor { /// This is true when the widget is has keyboard or IME focus. #[inline] pub fn has_input_focus(&self) -> bool { - self.part.has_key_focus || self.part.current.is_ime_enabled() + self.part.has_key_focus || self.common.current.is_ime_enabled() } /// Get whether the input state is erroneous From f51bdd2674f99e8d778b4dccaafa695c856be011 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 29 Apr 2026 10:44:42 +0000 Subject: [PATCH 05/10] Move has_key_focus to Common --- crates/kas-widgets/src/edit/editor.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 05687e043..13bcf1901 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -74,6 +74,7 @@ pub struct Common { direction: Direction, wrap: bool, read_only: bool, + has_key_focus: bool, edit_x_coord: Option, selection: CursorRange, last_edit: Option, @@ -93,6 +94,7 @@ impl Common { direction: Direction::Auto, wrap, read_only: false, + has_key_focus: false, edit_x_coord: None, selection: CursorRange::default(), last_edit: Some(EditOp::Initial), @@ -151,7 +153,6 @@ pub struct Part { display: TextDisplay, highlight: highlight::Cache, text: String, - has_key_focus: bool, } /// Inner editor interface @@ -377,7 +378,6 @@ impl Default for Part { display: TextDisplay::default(), highlight: Default::default(), text: Default::default(), - has_key_focus: false, } } } @@ -764,7 +764,7 @@ impl Part { Event::SelFocus(source) => { // NOTE: sel focus implies key focus since we only request // the latter. We must set before calling self.set_primary. - self.has_key_focus = true; + common.has_key_focus = true; if source == FocusSource::Pointer { self.set_primary(common, cx); } @@ -772,7 +772,7 @@ impl Part { return EventAction::Used; } Event::KeyFocus => { - self.has_key_focus = true; + common.has_key_focus = true; self.set_view_offset_from_cursor(common, cx); return if common.current.is_none() { @@ -786,7 +786,7 @@ impl Part { }; } Event::LostKeyFocus => { - self.has_key_focus = false; + common.has_key_focus = false; cx.redraw(); return if !common.current.is_ime_enabled() { EventAction::FocusLost @@ -861,7 +861,7 @@ impl Part { cx.cancel_ime_focus(&self.id); } } - return if !self.has_key_focus { + return if !common.has_key_focus { EventAction::FocusGained } else { EventAction::Used @@ -869,7 +869,7 @@ impl Part { } Ime::Disabled => { self.clear_ime(common); - return if !self.has_key_focus { + return if !common.has_key_focus { EventAction::FocusLost } else { EventAction::Used @@ -1213,7 +1213,7 @@ impl Part { /// Request key focus, if we don't have it or IME fn request_key_focus(&self, common: &Common, cx: &mut EventCx, source: FocusSource) { - if !self.has_key_focus && !common.current.is_ime_enabled() { + if !common.has_key_focus && !common.current.is_ime_enabled() { cx.request_key_focus(self.id.clone(), source); } } @@ -1536,7 +1536,7 @@ impl Part { /// Set primary clipboard (mouse buffer) contents from selection fn set_primary(&self, common: &Common, cx: &mut EventCx) { - if self.has_key_focus && !common.selection.is_empty() && cx.has_primary() { + if common.has_key_focus && !common.selection.is_empty() && cx.has_primary() { let range = common.selection.to_range(); cx.set_primary(String::from(&self.as_str()[range])); } @@ -1704,7 +1704,7 @@ impl Editor { /// This is true when the widget is has keyboard or IME focus. #[inline] pub fn has_input_focus(&self) -> bool { - self.part.has_key_focus || self.common.current.is_ime_enabled() + self.common.has_key_focus || self.common.current.is_ime_enabled() } /// Get whether the input state is erroneous From d92a29fa86ff42671b3929ba6ea8871c7b3c7918 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 29 Apr 2026 10:44:57 +0000 Subject: [PATCH 06/10] Move id from Part to Common --- crates/kas-widgets/src/edit/editor.rs | 50 +++++++++++---------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 13bcf1901..e851e25fa 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -68,6 +68,8 @@ impl EventAction { /// Editor state common to all parts #[derive(Debug)] pub struct Common { + /// We store a copy of the widget id here, since the latter is inaccessible + id: Id, colors: SchemeColors, font: FontSelector, dpem: f32, @@ -88,6 +90,7 @@ impl Common { #[inline] pub fn new(wrap: bool) -> Self { Common { + id: Id::default(), colors: SchemeColors::default(), font: FontSelector::default(), dpem: 16.0, @@ -107,7 +110,8 @@ impl Common { /// Configure `Common` data #[inline] #[must_use] - pub fn configure(&mut self, cx: &SizeCx) -> Option { + pub fn configure(&mut self, cx: &SizeCx, id: Id) -> Option { + self.id = id; let font = cx.font(TextClass::Editor); let dpem = cx.dpem(TextClass::Editor); if font != self.font || dpem != self.dpem { @@ -146,8 +150,6 @@ impl Common { /// methods: [`Self::content_size`], [`Self::draw_with_offset`]. #[autoimpl(Debug)] pub struct Part { - // TODO(opt): id is duplicated here since macros don't let us put the core here - id: Id, rect: Rect, status: Status, display: TextDisplay, @@ -288,7 +290,7 @@ impl Component { /// Configure component #[inline] pub fn configure(&mut self, cx: &mut ConfigCx, id: Id) { - if let Some(ActionResetStatus) = self.0.common.configure(&cx.size_cx()) { + if let Some(ActionResetStatus) = self.0.common.configure(&cx.size_cx(), id) { self.0.part.require_reprepare(); } if let Some(_) = self.1.configure(cx) { @@ -296,7 +298,6 @@ impl Component { self.0.part.require_reprepare(); } - self.0.part.configure(id); self.0.part.prepare_runs(&mut self.0.common, &mut self.1); } @@ -372,7 +373,6 @@ impl Default for Part { #[inline] fn default() -> Self { Part { - id: Id::default(), rect: Rect::ZERO, status: Status::New, display: TextDisplay::default(), @@ -402,7 +402,7 @@ impl Part { /// Get the base directionality of the text /// - /// [`Self::configure`] should be called before this method. + /// [`Self::prepare_runs`] should be called before this method. #[inline] pub fn text_is_rtl(&self) -> bool { debug_assert!(self.status >= Status::ResizeLevelRuns); @@ -421,19 +421,11 @@ impl Part { self.status = Status::New; } - /// Configure component - /// - /// [`Common::configure`] must be called before this method. - pub fn configure(&mut self, id: Id) { - self.id = id; - } - /// Perform run-breaking and shaping /// /// This represents a high-level step of preparation required before - /// displaying text. After the `Part` is [configured](Self::configure), this - /// method should be called before any sizing operations. This will advance - /// the [`Status`] to [`Status::LevelRuns`]. + /// displaying text. This method should be called before any sizing + /// operations. This will advance the [`Status`] to [`Status::LevelRuns`]. /// This method must be called again after any edits to the `Part`'s text. #[inline] pub fn prepare_runs(&mut self, common: &mut Common, highlighter: &mut H) { @@ -724,7 +716,7 @@ impl Part { draw.decorate_text(pos, rect, &self.display, &tokens[r0..]); } - if !common.read_only && draw.ev_state().has_input_focus(&self.id) == Some(true) { + if !common.read_only && draw.ev_state().has_input_focus(&common.id) == Some(true) { draw.text_cursor( pos, rect, @@ -779,7 +771,7 @@ impl Part { let hint = Default::default(); let purpose = ImePurpose::Normal; let surrounding_text = self.ime_surrounding_text(common); - cx.replace_ime_focus(self.id.clone(), hint, purpose, surrounding_text); + cx.replace_ime_focus(common.id.clone(), hint, purpose, surrounding_text); EventAction::FocusGained } else { EventAction::Used @@ -858,7 +850,7 @@ impl Part { } CurrentAction::Selection => { // Do not interrupt selection - cx.cancel_ime_focus(&self.id); + cx.cancel_ime_focus(&common.id); } } return if !common.has_key_focus { @@ -949,14 +941,14 @@ impl Part { } if let Some(text) = self.ime_surrounding_text(common) { - cx.update_ime_surrounding_text(&self.id, text); + cx.update_ime_surrounding_text(&common.id, text); } return EventAction::Used; } }, Event::PressStart(press) if press.is_tertiary() => { - return match press.grab_click(self.id.clone()).complete(cx) { + return match press.grab_click(common.id.clone()).complete(cx) { Unused => EventAction::Unused, Used => EventAction::Used, }; @@ -977,7 +969,7 @@ impl Part { } index.into() } - event => match common.input_handler.handle(cx, self.id.clone(), event) { + event => match common.input_handler.handle(cx, common.id.clone(), event) { TextInputAction::Used => return EventAction::Used, TextInputAction::Unused => return EventAction::Unused, TextInputAction::PressStart { @@ -987,7 +979,7 @@ impl Part { } => { if common.current.is_ime_enabled() { self.clear_ime(common); - cx.cancel_ime_focus(&self.id); + cx.cancel_ime_focus(&common.id); } self.request_key_focus(common, cx, FocusSource::Pointer); self.save_undo_state(common, Some(EditOp::Cursor)); @@ -1028,7 +1020,7 @@ impl Part { TextInputAction::PressEnd { coord } => { if common.current.is_ime_enabled() { self.clear_ime(common); - cx.cancel_ime_focus(&self.id); + cx.cancel_ime_focus(&common.id); } self.save_undo_state(common, Some(EditOp::Cursor)); if common.current == CurrentAction::Selection { @@ -1081,7 +1073,7 @@ impl Part { common.current = CurrentAction::None; } else if common.current.is_ime_enabled() { self.clear_ime(common); - cx.cancel_ime_focus(&self.id); + cx.cancel_ime_focus(&common.id); } } @@ -1192,7 +1184,7 @@ impl Part { return; }; - cx.set_ime_cursor_area(&self.id, rect + Offset::conv(self.rect.pos)); + cx.set_ime_cursor_area(&common.id, rect + Offset::conv(self.rect.pos)); } /// Call before an edit to (potentially) commit current state based on last_edit @@ -1214,7 +1206,7 @@ impl Part { /// Request key focus, if we don't have it or IME fn request_key_focus(&self, common: &Common, cx: &mut EventCx, source: FocusSource) { if !common.has_key_focus && !common.current.is_ime_enabled() { - cx.request_key_focus(self.id.clone(), source); + cx.request_key_focus(common.id.clone(), source); } } @@ -1565,7 +1557,7 @@ impl Editor { /// Get a reference to the widget's identifier #[inline] pub fn id_ref(&self) -> &Id { - &self.part.id + &self.common.id } /// Get the widget's identifier From c9047cbb91d983fc4533accc9800bbc0f0ea817d Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 29 Apr 2026 10:52:47 +0000 Subject: [PATCH 07/10] Move several Part methods to Common --- crates/kas-widgets/src/edit/editor.rs | 89 ++++++++++++++------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index e851e25fa..980d95c6d 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -573,7 +573,7 @@ impl Part { self.prepare_runs(common, highlighter); if self.prepare_wrap(common) { cx.resize(); - self.set_view_offset_from_cursor(common, cx); + common.set_view_offset_from_cursor(self, cx); } cx.redraw(); } @@ -747,7 +747,7 @@ impl Part { let range = match event { Event::NavFocus(source) if source == FocusSource::Key => { if !common.input_handler.is_selecting() { - self.request_key_focus(common, cx, source); + common.request_key_focus(cx, source); } return EventAction::Used; } @@ -758,14 +758,14 @@ impl Part { // the latter. We must set before calling self.set_primary. common.has_key_focus = true; if source == FocusSource::Pointer { - self.set_primary(common, cx); + common.set_primary(self, cx); } return EventAction::Used; } Event::KeyFocus => { common.has_key_focus = true; - self.set_view_offset_from_cursor(common, cx); + common.set_view_offset_from_cursor(self, cx); return if common.current.is_none() { let hint = Default::default(); @@ -789,7 +789,7 @@ impl Part { Event::LostSelFocus => { // NOTE: we can assume that we will receive Ime::Disabled if IME is active if !common.selection.is_empty() { - self.save_undo_state(common, None); + common.save_undo_state(self, None); common.selection.clear_selection(); } common.input_handler.stop_selecting(); @@ -799,7 +799,7 @@ impl Part { Event::Command(cmd, code) => match self.cmd_action(common, cx, cmd, code) { Ok(action) => { if matches!(action, EventAction::Cursor) { - self.set_view_offset_from_cursor(common, cx); + common.set_view_offset_from_cursor(self, cx); } return action; } @@ -809,7 +809,7 @@ impl Part { if event.state == ElementState::Pressed && !common.read_only => { return if let Some(text) = &event.text { - self.save_undo_state(common, Some(EditOp::KeyInput)); + common.save_undo_state(self, Some(EditOp::KeyInput)); self.cancel_selection_and_ime(common, cx); let selection = common.selection.to_range(); @@ -827,7 +827,7 @@ impl Part { match self.cmd_action(common, cx, cmd, Some(event.physical_key)) { Ok(action) => { if matches!(action, EventAction::Cursor) { - self.set_view_offset_from_cursor(common, cx); + common.set_view_offset_from_cursor(self, cx); } action } @@ -868,7 +868,7 @@ impl Part { }; } Ime::Preedit { text, cursor } => { - self.save_undo_state(common, None); + common.save_undo_state(self, None); let mut edit_range = match common.current.clone() { CurrentAction::ImeStart if cursor.is_some() => common.selection.to_range(), CurrentAction::ImeStart => return EventAction::Used, @@ -892,7 +892,7 @@ impl Part { return EventAction::Preedit; } Ime::Commit { text } => { - self.save_undo_state(common, Some(EditOp::Ime)); + common.save_undo_state(self, Some(EditOp::Ime)); let edit_range = match common.current.clone() { CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), @@ -912,7 +912,7 @@ impl Part { before_bytes, after_bytes, } => { - self.save_undo_state(common, None); + common.save_undo_state(self, None); let edit_range = match common.current.clone() { CurrentAction::ImeStart => common.selection.to_range(), CurrentAction::ImePreedit { edit_range } => edit_range.cast(), @@ -957,10 +957,10 @@ impl Part { let rel_pos = (press.coord - self.rect().pos).cast(); let mut index = self.display.text_index_nearest(rel_pos); self.cancel_selection_and_ime(common, cx); - self.request_key_focus(common, cx, FocusSource::Pointer); + common.request_key_focus(cx, FocusSource::Pointer); if let Some(content) = cx.get_primary() { - self.save_undo_state(common, Some(EditOp::Clipboard)); + common.save_undo_state(self, Some(EditOp::Clipboard)); let range = self.trim_paste(common.wrap, &content); self.replace_range(index..index, &content[range.clone()]); @@ -981,8 +981,8 @@ impl Part { self.clear_ime(common); cx.cancel_ime_focus(&common.id); } - self.request_key_focus(common, cx, FocusSource::Pointer); - self.save_undo_state(common, Some(EditOp::Cursor)); + common.request_key_focus(cx, FocusSource::Pointer); + common.save_undo_state(self, Some(EditOp::Cursor)); common.current = CurrentAction::Selection; let rel_pos = (coord - self.rect().pos).cast(); @@ -1022,9 +1022,9 @@ impl Part { self.clear_ime(common); cx.cancel_ime_focus(&common.id); } - self.save_undo_state(common, Some(EditOp::Cursor)); + common.save_undo_state(self, Some(EditOp::Cursor)); if common.current == CurrentAction::Selection { - self.set_primary(common, cx); + common.set_primary(self, cx); } else { let rel_pos = (coord - self.rect().pos).cast(); let index = self.display.text_index_nearest(rel_pos); @@ -1033,7 +1033,7 @@ impl Part { } common.current = CurrentAction::None; - self.request_key_focus(common, cx, FocusSource::Pointer); + common.request_key_focus(cx, FocusSource::Pointer); return EventAction::Used; } }, @@ -1041,7 +1041,7 @@ impl Part { if range != common.selection { common.selection = range; - self.set_view_offset_from_cursor(common, cx); + common.set_view_offset_from_cursor(self, cx); common.edit_x_coord = None; cx.redraw(); } @@ -1186,30 +1186,33 @@ impl Part { cx.set_ime_cursor_area(&common.id, rect + Offset::conv(self.rect.pos)); } +} +impl Common { /// Call before an edit to (potentially) commit current state based on last_edit /// /// Call with [`None`] to force commit of any uncommitted changes. - fn save_undo_state(&mut self, common: &mut Common, edit: Option) { + fn save_undo_state(&mut self, part: &mut Part, edit: Option) { if let Some(op) = edit - && op.try_merge(&mut common.last_edit) + && op.try_merge(&mut self.last_edit) { return; } - common.last_edit = edit; - common - .undo_stack - .try_push((self.as_str().to_string(), common.selection)); + self.last_edit = edit; + self.undo_stack + .try_push((part.as_str().to_string(), self.selection)); } /// Request key focus, if we don't have it or IME - fn request_key_focus(&self, common: &Common, cx: &mut EventCx, source: FocusSource) { - if !common.has_key_focus && !common.current.is_ime_enabled() { - cx.request_key_focus(common.id.clone(), source); + fn request_key_focus(&self, cx: &mut EventCx, source: FocusSource) { + if !self.has_key_focus && !self.current.is_ime_enabled() { + cx.request_key_focus(self.id.clone(), source); } } +} +impl Part { fn trim_paste(&self, wrap: bool, text: &str) -> Range { let mut end = text.len(); if !wrap { @@ -1455,7 +1458,7 @@ impl Part { // We can receive some commands without key focus as a result of // selection focus. Request focus on edit actions (like Command::Cut). if !matches!(action, Action::None | Action::Deselect) { - self.request_key_focus(common, cx, FocusSource::Synthetic); + common.request_key_focus(cx, FocusSource::Synthetic); } if !matches!(action, Action::None) { @@ -1468,7 +1471,7 @@ impl Part { Action::Activate | Action::UndoRedo(_) => None, Action::Insert(_, edit) | Action::Delete(_, edit) => Some(edit), }; - self.save_undo_state(common, edit_op); + common.save_undo_state(self, edit_op); let action = match action { Action::None => unreachable!(), @@ -1502,7 +1505,7 @@ impl Part { if !shift { common.selection.clear_selection(); } else { - self.set_primary(common, cx); + common.set_primary(self, cx); } common.edit_x_coord = x_coord; cx.redraw(); @@ -1525,12 +1528,14 @@ impl Part { Ok(action) } +} +impl Common { /// Set primary clipboard (mouse buffer) contents from selection - fn set_primary(&self, common: &Common, cx: &mut EventCx) { - if common.has_key_focus && !common.selection.is_empty() && cx.has_primary() { - let range = common.selection.to_range(); - cx.set_primary(String::from(&self.as_str()[range])); + fn set_primary(&self, part: &Part, cx: &mut EventCx) { + if self.has_key_focus && !self.selection.is_empty() && cx.has_primary() { + let range = self.selection.to_range(); + cx.set_primary(String::from(&part.as_str()[range])); } } @@ -1539,13 +1544,13 @@ impl Part { /// It is assumed that the text has not changed. /// /// A redraw is assumed since the cursor moved. - fn set_view_offset_from_cursor(&mut self, common: &Common, cx: &mut EventCx) { - let cursor = common.selection.cursor; - if self.is_prepared() - && let Some(marker) = self.display.text_glyph_pos(cursor).next_back() + fn set_view_offset_from_cursor(&self, part: &Part, cx: &mut EventCx) { + let cursor = self.selection.cursor; + if part.is_prepared() + && let Some(marker) = part.display.text_glyph_pos(cursor).next_back() { let y0 = (marker.pos.1 - marker.ascent).cast_floor(); - let pos = self.rect.pos + Offset(marker.pos.0.cast_nearest(), y0); + let pos = part.rect.pos + Offset(marker.pos.0.cast_nearest(), y0); let size = Size(0, i32::conv_ceil(marker.pos.1 - marker.descent) - y0); cx.set_scroll(Scroll::Rect(Rect { pos, size })); } @@ -1593,8 +1598,8 @@ impl Editor { /// [`Self::set_string`] to commit changes to the undo history. #[inline] pub fn pre_commit(&mut self) { - self.part - .save_undo_state(&mut self.common, Some(EditOp::Synthetic)); + self.common + .save_undo_state(&mut self.part, Some(EditOp::Synthetic)); } /// Clear text contents and undo history From c95609ed0388e4b4906aa2abbaf5c08020a1eb65 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Fri, 1 May 2026 07:31:35 +0000 Subject: [PATCH 08/10] Move code for methods remaining in Part --- crates/kas-widgets/src/edit/editor.rs | 282 +++++++++++++------------- 1 file changed, 141 insertions(+), 141 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index 980d95c6d..d738b1a58 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -727,6 +727,147 @@ impl Part { } } + /// Replace a section of text + /// + /// This may be used to edit the raw text instead of replacing it. + /// One must call [`Text::prepare`] afterwards. + /// + /// One may simulate an unbounded range by via `start..usize::MAX`. + /// + /// Currently this is not significantly more efficient than + /// [`Text::set_text`]. This may change in the future (TODO). + #[inline] + fn replace_range(&mut self, range: std::ops::Range, replace_with: &str) { + self.text.replace_range(range, replace_with); + self.require_reprepare(); + } + + fn trim_paste(&self, wrap: bool, text: &str) -> Range { + let mut end = text.len(); + if !wrap { + // We cut the content short on control characters and + // ignore them (preventing line-breaks and ignoring any + // actions such as recursive-paste). + for (i, c) in text.char_indices() { + if c < '\u{20}' || ('\u{7f}'..='\u{9f}').contains(&c) { + end = i; + break; + } + } + } + 0..end + } + + /// Clean up IME state + /// + /// One should also call [`EventCx::cancel_ime_focus`] unless this is + /// implied. + fn clear_ime(&mut self, common: &mut Common) { + if common.current.is_ime_enabled() { + let action = std::mem::replace(&mut common.current, CurrentAction::None); + if let CurrentAction::ImePreedit { edit_range } = action { + common.selection.set_position(edit_range.start.cast()); + self.replace_range(edit_range.cast(), ""); + } + } + } + + fn ime_surrounding_text(&self, common: &Common) -> Option { + const MAX_TEXT_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES; + + let sel_range = common.selection.to_range(); + let edit_range = match common.current.clone() { + CurrentAction::ImePreedit { edit_range } => Some(edit_range.cast()), + _ => None, + }; + let mut range = edit_range.clone().unwrap_or(sel_range); + let initial_range = range.clone(); + let edit_len = edit_range.clone().map(|r| r.len()).unwrap_or(0); + + if self.status >= Status::Wrapped { + if let Some((_, line_range)) = self.display.find_line(range.start) { + range.start = line_range.start; + } + if let Some((_, line_range)) = self.display.find_line(range.end) { + range.end = line_range.end; + } + } + + if range.len() - edit_len > MAX_TEXT_BYTES { + range.end = range.end.min(initial_range.end + MAX_TEXT_BYTES / 2); + while !self.as_str().is_char_boundary(range.end) { + range.end -= 1; + } + + if range.len() - edit_len > MAX_TEXT_BYTES { + range.start = range.start.max(initial_range.start - MAX_TEXT_BYTES / 2); + while !self.as_str().is_char_boundary(range.start) { + range.start += 1; + } + } + } + + let start = range.start; + let mut text = String::with_capacity(range.len() - edit_len); + if let Some(er) = edit_range { + text.push_str(&self.as_str()[range.start..er.start]); + text.push_str(&self.as_str()[er.end..range.end]); + } else { + text = self.as_str()[range].to_string(); + } + + let cursor = common.selection.cursor.saturating_sub(start); + // Terminology difference: our sel_index is called 'anchor' + let sel_index = common.selection.anchor.saturating_sub(start); + ImeSurroundingText::new(text, cursor, sel_index) + .inspect_err(|err| { + // TODO: use Display for err not Debug + log::warn!("Editor::ime_surrounding_text failed: {err:?}") + }) + .ok() + } + + /// Call to set IME position only while IME is active + fn set_ime_cursor_area(&self, common: &Common, cx: &mut EventState) { + if !self.is_prepared() { + return; + } + + let range = match common.current.clone() { + CurrentAction::ImeStart => common.selection.to_range(), + CurrentAction::ImePreedit { edit_range } => edit_range.cast(), + _ => return, + }; + + let (m1, m2); + if range.is_empty() { + let mut iter = self.display.text_glyph_pos(range.start); + m1 = iter.next(); + m2 = iter.next(); + } else { + m1 = self.display.text_glyph_pos(range.start).next_back(); + m2 = self.display.text_glyph_pos(range.end).next(); + } + + let rect = if let Some((c1, c2)) = m1.zip(m2) { + let left = c1.pos.0.min(c2.pos.0); + let right = c1.pos.0.max(c2.pos.0); + let top = (c1.pos.1 - c1.ascent).min(c2.pos.1 - c2.ascent); + let bottom = (c1.pos.1 - c1.descent).max(c2.pos.1 - c2.ascent); + let p1 = Vec2(left, top).cast_floor(); + let p2 = Vec2(right, bottom).cast_ceil(); + Rect::from_coords(p1, p2) + } else if let Some(c) = m1.or(m2) { + let p1 = Vec2(c.pos.0, c.pos.1 - c.ascent).cast_floor(); + let p2 = Vec2(c.pos.0, c.pos.1 - c.descent).cast_ceil(); + Rect::from_coords(p1, p2) + } else { + return; + }; + + cx.set_ime_cursor_area(&common.id, rect + Offset::conv(self.rect.pos)); + } + /// Handle an event /// /// If [`EventAction::requires_repreparation`] then the caller **must** call @@ -1048,21 +1189,6 @@ impl Part { event_action } - /// Replace a section of text - /// - /// This may be used to edit the raw text instead of replacing it. - /// One must call [`Text::prepare`] afterwards. - /// - /// One may simulate an unbounded range by via `start..usize::MAX`. - /// - /// Currently this is not significantly more efficient than - /// [`Text::set_text`]. This may change in the future (TODO). - #[inline] - fn replace_range(&mut self, range: std::ops::Range, replace_with: &str) { - self.text.replace_range(range, replace_with); - self.require_reprepare(); - } - /// Cancel on-going selection and IME actions /// /// This should be called if e.g. key-input interrupts the current @@ -1076,116 +1202,6 @@ impl Part { cx.cancel_ime_focus(&common.id); } } - - /// Clean up IME state - /// - /// One should also call [`EventCx::cancel_ime_focus`] unless this is - /// implied. - fn clear_ime(&mut self, common: &mut Common) { - if common.current.is_ime_enabled() { - let action = std::mem::replace(&mut common.current, CurrentAction::None); - if let CurrentAction::ImePreedit { edit_range } = action { - common.selection.set_position(edit_range.start.cast()); - self.replace_range(edit_range.cast(), ""); - } - } - } - - fn ime_surrounding_text(&self, common: &Common) -> Option { - const MAX_TEXT_BYTES: usize = ImeSurroundingText::MAX_TEXT_BYTES; - - let sel_range = common.selection.to_range(); - let edit_range = match common.current.clone() { - CurrentAction::ImePreedit { edit_range } => Some(edit_range.cast()), - _ => None, - }; - let mut range = edit_range.clone().unwrap_or(sel_range); - let initial_range = range.clone(); - let edit_len = edit_range.clone().map(|r| r.len()).unwrap_or(0); - - if self.status >= Status::Wrapped { - if let Some((_, line_range)) = self.display.find_line(range.start) { - range.start = line_range.start; - } - if let Some((_, line_range)) = self.display.find_line(range.end) { - range.end = line_range.end; - } - } - - if range.len() - edit_len > MAX_TEXT_BYTES { - range.end = range.end.min(initial_range.end + MAX_TEXT_BYTES / 2); - while !self.as_str().is_char_boundary(range.end) { - range.end -= 1; - } - - if range.len() - edit_len > MAX_TEXT_BYTES { - range.start = range.start.max(initial_range.start - MAX_TEXT_BYTES / 2); - while !self.as_str().is_char_boundary(range.start) { - range.start += 1; - } - } - } - - let start = range.start; - let mut text = String::with_capacity(range.len() - edit_len); - if let Some(er) = edit_range { - text.push_str(&self.as_str()[range.start..er.start]); - text.push_str(&self.as_str()[er.end..range.end]); - } else { - text = self.as_str()[range].to_string(); - } - - let cursor = common.selection.cursor.saturating_sub(start); - // Terminology difference: our sel_index is called 'anchor' - let sel_index = common.selection.anchor.saturating_sub(start); - ImeSurroundingText::new(text, cursor, sel_index) - .inspect_err(|err| { - // TODO: use Display for err not Debug - log::warn!("Editor::ime_surrounding_text failed: {err:?}") - }) - .ok() - } - - /// Call to set IME position only while IME is active - fn set_ime_cursor_area(&self, common: &Common, cx: &mut EventState) { - if !self.is_prepared() { - return; - } - - let range = match common.current.clone() { - CurrentAction::ImeStart => common.selection.to_range(), - CurrentAction::ImePreedit { edit_range } => edit_range.cast(), - _ => return, - }; - - let (m1, m2); - if range.is_empty() { - let mut iter = self.display.text_glyph_pos(range.start); - m1 = iter.next(); - m2 = iter.next(); - } else { - m1 = self.display.text_glyph_pos(range.start).next_back(); - m2 = self.display.text_glyph_pos(range.end).next(); - } - - let rect = if let Some((c1, c2)) = m1.zip(m2) { - let left = c1.pos.0.min(c2.pos.0); - let right = c1.pos.0.max(c2.pos.0); - let top = (c1.pos.1 - c1.ascent).min(c2.pos.1 - c2.ascent); - let bottom = (c1.pos.1 - c1.descent).max(c2.pos.1 - c2.ascent); - let p1 = Vec2(left, top).cast_floor(); - let p2 = Vec2(right, bottom).cast_ceil(); - Rect::from_coords(p1, p2) - } else if let Some(c) = m1.or(m2) { - let p1 = Vec2(c.pos.0, c.pos.1 - c.ascent).cast_floor(); - let p2 = Vec2(c.pos.0, c.pos.1 - c.descent).cast_ceil(); - Rect::from_coords(p1, p2) - } else { - return; - }; - - cx.set_ime_cursor_area(&common.id, rect + Offset::conv(self.rect.pos)); - } } impl Common { @@ -1213,22 +1229,6 @@ impl Common { } impl Part { - fn trim_paste(&self, wrap: bool, text: &str) -> Range { - let mut end = text.len(); - if !wrap { - // We cut the content short on control characters and - // ignore them (preventing line-breaks and ignoring any - // actions such as recursive-paste). - for (i, c) in text.char_indices() { - if c < '\u{20}' || ('\u{7f}'..='\u{9f}').contains(&c) { - end = i; - break; - } - } - } - 0..end - } - /// Drive action of a [`Command`] fn cmd_action( &mut self, From 9e6fc49459b3678e3e7809b8b9d478de94fad259 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 29 Apr 2026 14:13:42 +0000 Subject: [PATCH 09/10] Add fn Part::handle_ime --- crates/kas-widgets/src/edit/editor.rs | 227 ++++++++++++++------------ 1 file changed, 119 insertions(+), 108 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index d738b1a58..a7e79f0da 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -868,11 +868,122 @@ impl Part { cx.set_ime_cursor_area(&common.id, rect + Offset::conv(self.rect.pos)); } + /// Handle IME methods on a given `Part`. + fn handle_ime(&mut self, common: &mut Common, cx: &mut EventCx, event: Ime) -> EventAction { + match event { + Ime::Enabled => { + match common.current { + CurrentAction::None => { + common.current = CurrentAction::ImeStart; + self.set_ime_cursor_area(common, cx); + } + CurrentAction::ImeStart | CurrentAction::ImePreedit { .. } => { + // already enabled + } + CurrentAction::Selection => { + unreachable!() + } + } + if !common.has_key_focus { + EventAction::FocusGained + } else { + EventAction::Used + } + } + Ime::Disabled => { + self.clear_ime(common); + if !common.has_key_focus { + EventAction::FocusLost + } else { + EventAction::Used + } + } + Ime::Preedit { text, cursor } => { + common.save_undo_state(self, None); + let mut edit_range = match common.current.clone() { + CurrentAction::ImeStart if cursor.is_some() => common.selection.to_range(), + CurrentAction::ImeStart => return EventAction::Used, + CurrentAction::ImePreedit { edit_range } => edit_range.cast(), + _ => return EventAction::Used, + }; + + self.replace_range(edit_range.clone(), text); + edit_range.end = edit_range.start + text.len(); + if let Some((start, end)) = cursor { + common.selection.anchor = edit_range.start + start; + common.selection.cursor = edit_range.start + end; + } else { + common.selection.set_position(edit_range.start + text.len()); + } + + common.current = CurrentAction::ImePreedit { + edit_range: edit_range.cast(), + }; + common.edit_x_coord = None; + EventAction::Preedit + } + Ime::Commit { text } => { + common.save_undo_state(self, Some(EditOp::Ime)); + let edit_range = match common.current.clone() { + CurrentAction::ImeStart => common.selection.to_range(), + CurrentAction::ImePreedit { edit_range } => edit_range.cast(), + _ => return EventAction::Used, + }; + + self.replace_range(edit_range.clone(), text); + common.selection.set_position(edit_range.start + text.len()); + + common.current = CurrentAction::ImePreedit { + edit_range: common.selection.to_range().cast(), + }; + common.edit_x_coord = None; + EventAction::Edit + } + Ime::DeleteSurrounding { + before_bytes, + after_bytes, + } => { + common.save_undo_state(self, None); + let edit_range = match common.current.clone() { + CurrentAction::ImeStart => common.selection.to_range(), + CurrentAction::ImePreedit { edit_range } => edit_range.cast(), + _ => return EventAction::Used, + }; + + if before_bytes > 0 { + let end = edit_range.start; + let start = end - before_bytes; + if self.as_str().is_char_boundary(start) { + self.replace_range(start..end, ""); + common.selection.delete_range(start..end); + } else { + log::warn!("buggy IME tried to delete range not at char boundary"); + } + } + + if after_bytes > 0 { + let start = edit_range.end; + let end = start + after_bytes; + if self.as_str().is_char_boundary(end) { + self.replace_range(start..end, ""); + } else { + log::warn!("buggy IME tried to delete range not at char boundary"); + } + } + + if let Some(text) = self.ime_surrounding_text(common) { + cx.update_ime_surrounding_text(&common.id, text); + } + + EventAction::Used + } + } + } + /// Handle an event /// /// If [`EventAction::requires_repreparation`] then the caller **must** call /// re-prepare the text by calling [`Self::prepare_and_scroll`]. - #[inline] pub fn handle_event( &mut self, common: &mut Common, @@ -979,115 +1090,15 @@ impl Part { } }; } - Event::Ime(ime) => match ime { - Ime::Enabled => { - match common.current { - CurrentAction::None => { - common.current = CurrentAction::ImeStart; - self.set_ime_cursor_area(common, cx); - } - CurrentAction::ImeStart | CurrentAction::ImePreedit { .. } => { - // already enabled - } - CurrentAction::Selection => { - // Do not interrupt selection - cx.cancel_ime_focus(&common.id); - } - } - return if !common.has_key_focus { - EventAction::FocusGained - } else { - EventAction::Used - }; - } - Ime::Disabled => { - self.clear_ime(common); - return if !common.has_key_focus { - EventAction::FocusLost - } else { - EventAction::Used - }; + Event::Ime(ime) => { + if matches!(common.current, CurrentAction::Selection) { + // Selection takes priority over IME + cx.cancel_ime_focus(&common.id); + return EventAction::Unused; } - Ime::Preedit { text, cursor } => { - common.save_undo_state(self, None); - let mut edit_range = match common.current.clone() { - CurrentAction::ImeStart if cursor.is_some() => common.selection.to_range(), - CurrentAction::ImeStart => return EventAction::Used, - CurrentAction::ImePreedit { edit_range } => edit_range.cast(), - _ => return EventAction::Used, - }; - - self.replace_range(edit_range.clone(), text); - edit_range.end = edit_range.start + text.len(); - if let Some((start, end)) = cursor { - common.selection.anchor = edit_range.start + start; - common.selection.cursor = edit_range.start + end; - } else { - common.selection.set_position(edit_range.start + text.len()); - } - - common.current = CurrentAction::ImePreedit { - edit_range: edit_range.cast(), - }; - common.edit_x_coord = None; - return EventAction::Preedit; - } - Ime::Commit { text } => { - common.save_undo_state(self, Some(EditOp::Ime)); - let edit_range = match common.current.clone() { - CurrentAction::ImeStart => common.selection.to_range(), - CurrentAction::ImePreedit { edit_range } => edit_range.cast(), - _ => return EventAction::Used, - }; - self.replace_range(edit_range.clone(), text); - common.selection.set_position(edit_range.start + text.len()); - - common.current = CurrentAction::ImePreedit { - edit_range: common.selection.to_range().cast(), - }; - common.edit_x_coord = None; - return EventAction::Edit; - } - Ime::DeleteSurrounding { - before_bytes, - after_bytes, - } => { - common.save_undo_state(self, None); - let edit_range = match common.current.clone() { - CurrentAction::ImeStart => common.selection.to_range(), - CurrentAction::ImePreedit { edit_range } => edit_range.cast(), - _ => return EventAction::Used, - }; - - if before_bytes > 0 { - let end = edit_range.start; - let start = end - before_bytes; - if self.as_str().is_char_boundary(start) { - self.replace_range(start..end, ""); - common.selection.delete_range(start..end); - } else { - log::warn!("buggy IME tried to delete range not at char boundary"); - } - } - - if after_bytes > 0 { - let start = edit_range.end; - let end = start + after_bytes; - if self.as_str().is_char_boundary(end) { - self.replace_range(start..end, ""); - } else { - log::warn!("buggy IME tried to delete range not at char boundary"); - } - } - - if let Some(text) = self.ime_surrounding_text(common) { - cx.update_ime_surrounding_text(&common.id, text); - } - - return EventAction::Used; - } - }, + return self.handle_ime(common, cx, ime); + } Event::PressStart(press) if press.is_tertiary() => { return match press.grab_click(common.id.clone()).complete(cx) { Unused => EventAction::Unused, From a871adb2b96ca7c1885a832a69009e76a5c49b2c Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 29 Apr 2026 13:59:42 +0000 Subject: [PATCH 10/10] Move more Part methods to Common --- crates/kas-widgets/src/edit/editor.rs | 290 ++++++++++++-------------- 1 file changed, 139 insertions(+), 151 deletions(-) diff --git a/crates/kas-widgets/src/edit/editor.rs b/crates/kas-widgets/src/edit/editor.rs index a7e79f0da..96fdfe678 100644 --- a/crates/kas-widgets/src/edit/editor.rs +++ b/crates/kas-widgets/src/edit/editor.rs @@ -353,7 +353,7 @@ impl Component { /// Handle an event #[inline] pub fn handle_event(&mut self, cx: &mut EventCx, event: Event) -> EventAction { - let action = self.0.part.handle_event(&mut self.0.common, cx, event); + let action = self.0.common.handle_event(&mut self.0.part, cx, event); if action.requires_repreparation() { self.0 .part @@ -979,18 +979,15 @@ impl Part { } } } +} +impl Common { /// Handle an event /// /// If [`EventAction::requires_repreparation`] then the caller **must** call - /// re-prepare the text by calling [`Self::prepare_and_scroll`]. - pub fn handle_event( - &mut self, - common: &mut Common, - cx: &mut EventCx, - event: Event, - ) -> EventAction { - if !self.is_prepared() { + /// re-prepare the text by calling [`Part::prepare_and_scroll`]. + pub fn handle_event(&mut self, part: &mut Part, cx: &mut EventCx, event: Event) -> EventAction { + if !part.is_prepared() { debug_assert!(false); return EventAction::Unused; } @@ -998,8 +995,8 @@ impl Part { let mut event_action = EventAction::Used; let range = match event { Event::NavFocus(source) if source == FocusSource::Key => { - if !common.input_handler.is_selecting() { - common.request_key_focus(cx, source); + if !self.input_handler.is_selecting() { + self.request_key_focus(cx, source); } return EventAction::Used; } @@ -1008,31 +1005,31 @@ impl Part { Event::SelFocus(source) => { // NOTE: sel focus implies key focus since we only request // the latter. We must set before calling self.set_primary. - common.has_key_focus = true; + self.has_key_focus = true; if source == FocusSource::Pointer { - common.set_primary(self, cx); + self.set_primary(part, cx); } return EventAction::Used; } Event::KeyFocus => { - common.has_key_focus = true; - common.set_view_offset_from_cursor(self, cx); + self.has_key_focus = true; + self.set_view_offset_from_cursor(part, cx); - return if common.current.is_none() { + return if self.current.is_none() { let hint = Default::default(); let purpose = ImePurpose::Normal; - let surrounding_text = self.ime_surrounding_text(common); - cx.replace_ime_focus(common.id.clone(), hint, purpose, surrounding_text); + let surrounding_text = part.ime_surrounding_text(self); + cx.replace_ime_focus(self.id.clone(), hint, purpose, surrounding_text); EventAction::FocusGained } else { EventAction::Used }; } Event::LostKeyFocus => { - common.has_key_focus = false; + self.has_key_focus = false; cx.redraw(); - return if !common.current.is_ime_enabled() { + return if !self.current.is_ime_enabled() { EventAction::FocusLost } else { EventAction::Used @@ -1040,34 +1037,32 @@ impl Part { } Event::LostSelFocus => { // NOTE: we can assume that we will receive Ime::Disabled if IME is active - if !common.selection.is_empty() { - common.save_undo_state(self, None); - common.selection.clear_selection(); + if !self.selection.is_empty() { + self.save_undo_state(part, None); + self.selection.clear_selection(); } - common.input_handler.stop_selecting(); + self.input_handler.stop_selecting(); cx.redraw(); return EventAction::Used; } - Event::Command(cmd, code) => match self.cmd_action(common, cx, cmd, code) { + Event::Command(cmd, code) => match self.cmd_action(part, cx, cmd, code) { Ok(action) => { if matches!(action, EventAction::Cursor) { - common.set_view_offset_from_cursor(self, cx); + self.set_view_offset_from_cursor(part, cx); } return action; } Err(NotReady) => return EventAction::Used, }, - Event::Key(event, false) - if event.state == ElementState::Pressed && !common.read_only => - { + Event::Key(event, false) if event.state == ElementState::Pressed && !self.read_only => { return if let Some(text) = &event.text { - common.save_undo_state(self, Some(EditOp::KeyInput)); - self.cancel_selection_and_ime(common, cx); + self.save_undo_state(part, Some(EditOp::KeyInput)); + self.cancel_selection_and_ime(part, cx); - let selection = common.selection.to_range(); - self.replace_range(selection.clone(), text); - common.selection.set_position(selection.start + text.len()); - common.edit_x_coord = None; + let selection = self.selection.to_range(); + part.replace_range(selection.clone(), text); + self.selection.set_position(selection.start + text.len()); + self.edit_x_coord = None; EventAction::Edit } else { @@ -1076,10 +1071,10 @@ impl Part { .shortcuts() .try_match_event(cx.modifiers(), event); if let Some(cmd) = opt_cmd { - match self.cmd_action(common, cx, cmd, Some(event.physical_key)) { + match self.cmd_action(part, cx, cmd, Some(event.physical_key)) { Ok(action) => { if matches!(action, EventAction::Cursor) { - common.set_view_offset_from_cursor(self, cx); + self.set_view_offset_from_cursor(part, cx); } action } @@ -1091,37 +1086,37 @@ impl Part { }; } Event::Ime(ime) => { - if matches!(common.current, CurrentAction::Selection) { + if matches!(self.current, CurrentAction::Selection) { // Selection takes priority over IME - cx.cancel_ime_focus(&common.id); + cx.cancel_ime_focus(&self.id); return EventAction::Unused; } - return self.handle_ime(common, cx, ime); + return part.handle_ime(self, cx, ime); } Event::PressStart(press) if press.is_tertiary() => { - return match press.grab_click(common.id.clone()).complete(cx) { + return match press.grab_click(self.id.clone()).complete(cx) { Unused => EventAction::Unused, Used => EventAction::Used, }; } Event::PressEnd { press, .. } if press.is_tertiary() => { - let rel_pos = (press.coord - self.rect().pos).cast(); - let mut index = self.display.text_index_nearest(rel_pos); - self.cancel_selection_and_ime(common, cx); - common.request_key_focus(cx, FocusSource::Pointer); + let rel_pos = (press.coord - part.rect().pos).cast(); + let mut index = part.display.text_index_nearest(rel_pos); + self.cancel_selection_and_ime(part, cx); + self.request_key_focus(cx, FocusSource::Pointer); if let Some(content) = cx.get_primary() { - common.save_undo_state(self, Some(EditOp::Clipboard)); + self.save_undo_state(part, Some(EditOp::Clipboard)); - let range = self.trim_paste(common.wrap, &content); - self.replace_range(index..index, &content[range.clone()]); + let range = part.trim_paste(self.wrap, &content); + part.replace_range(index..index, &content[range.clone()]); index += range.len(); event_action = EventAction::Edit; } index.into() } - event => match common.input_handler.handle(cx, common.id.clone(), event) { + event => match self.input_handler.handle(cx, self.id.clone(), event) { TextInputAction::Used => return EventAction::Used, TextInputAction::Unused => return EventAction::Unused, TextInputAction::PressStart { @@ -1129,72 +1124,72 @@ impl Part { clear, repeats, } => { - if common.current.is_ime_enabled() { - self.clear_ime(common); - cx.cancel_ime_focus(&common.id); + if self.current.is_ime_enabled() { + part.clear_ime(self); + cx.cancel_ime_focus(&self.id); } - common.request_key_focus(cx, FocusSource::Pointer); - common.save_undo_state(self, Some(EditOp::Cursor)); - common.current = CurrentAction::Selection; + self.request_key_focus(cx, FocusSource::Pointer); + self.save_undo_state(part, Some(EditOp::Cursor)); + self.current = CurrentAction::Selection; - let rel_pos = (coord - self.rect().pos).cast(); - let cursor = self.display.text_index_nearest(rel_pos); - let anchor = if clear { cursor } else { common.selection.anchor }; + let rel_pos = (coord - part.rect().pos).cast(); + let cursor = part.display.text_index_nearest(rel_pos); + let anchor = if clear { cursor } else { self.selection.anchor }; let range = CursorRange::from(anchor..cursor); if repeats > 1 { TextInput::expand_range( - self.text.as_str(), + part.text.as_str(), range, (repeats >= 3) - .then_some(&|index| self.display.find_line(index).map(|r| r.1)), + .then_some(&|index| part.display.find_line(index).map(|r| r.1)), ) } else { range } } TextInputAction::PressMove { coord, repeats } => { - if common.current != CurrentAction::Selection { + if self.current != CurrentAction::Selection { return EventAction::Used; } - let rel_pos = (coord - self.rect().pos).cast(); - let index = self.display.text_index_nearest(rel_pos); + let rel_pos = (coord - part.rect().pos).cast(); + let index = part.display.text_index_nearest(rel_pos); TextInput::adjust_range( - self.text.as_str(), - common.selection, + part.text.as_str(), + self.selection, index, repeats, - Some(&|index| self.display.find_line(index).map(|r| r.1)), + Some(&|index| part.display.find_line(index).map(|r| r.1)), ) } TextInputAction::PressEnd { coord } => { - if common.current.is_ime_enabled() { - self.clear_ime(common); - cx.cancel_ime_focus(&common.id); + if self.current.is_ime_enabled() { + part.clear_ime(self); + cx.cancel_ime_focus(&self.id); } - common.save_undo_state(self, Some(EditOp::Cursor)); - if common.current == CurrentAction::Selection { - common.set_primary(self, cx); + self.save_undo_state(part, Some(EditOp::Cursor)); + if self.current == CurrentAction::Selection { + self.set_primary(part, cx); } else { - let rel_pos = (coord - self.rect().pos).cast(); - let index = self.display.text_index_nearest(rel_pos); - common.selection.cursor = index; - common.selection.clear_selection(); + let rel_pos = (coord - part.rect().pos).cast(); + let index = part.display.text_index_nearest(rel_pos); + self.selection.cursor = index; + self.selection.clear_selection(); } - common.current = CurrentAction::None; + self.current = CurrentAction::None; - common.request_key_focus(cx, FocusSource::Pointer); + self.request_key_focus(cx, FocusSource::Pointer); return EventAction::Used; } }, }; - if range != common.selection { - common.selection = range; - common.set_view_offset_from_cursor(self, cx); - common.edit_x_coord = None; + if range != self.selection { + self.selection = range; + self.set_view_offset_from_cursor(part, cx); + self.edit_x_coord = None; cx.redraw(); } event_action @@ -1204,18 +1199,16 @@ impl Part { /// /// This should be called if e.g. key-input interrupts the current /// action. - fn cancel_selection_and_ime(&mut self, common: &mut Common, cx: &mut EventState) { - if common.current == CurrentAction::Selection { - common.input_handler.stop_selecting(); - common.current = CurrentAction::None; - } else if common.current.is_ime_enabled() { - self.clear_ime(common); - cx.cancel_ime_focus(&common.id); + fn cancel_selection_and_ime(&mut self, part: &mut Part, cx: &mut EventState) { + if self.current == CurrentAction::Selection { + self.input_handler.stop_selecting(); + self.current = CurrentAction::None; + } else if self.current.is_ime_enabled() { + part.clear_ime(self); + cx.cancel_ime_focus(&self.id); } } -} -impl Common { /// Call before an edit to (potentially) commit current state based on last_edit /// /// Call with [`None`] to force commit of any uncommitted changes. @@ -1237,30 +1230,27 @@ impl Common { cx.request_key_focus(self.id.clone(), source); } } -} - -impl Part { /// Drive action of a [`Command`] fn cmd_action( &mut self, - common: &mut Common, + part: &mut Part, cx: &mut EventCx, mut cmd: Command, code: Option, ) -> Result { - debug_assert!(self.is_prepared()); + debug_assert!(part.is_prepared()); - let editable = !common.read_only; + let editable = !self.read_only; let mut shift = cx.modifiers().shift_key(); let mut buf = [0u8; 4]; - let cursor = common.selection.cursor; - let len = self.as_str().len(); - let multi_line = common.wrap; - let selection = common.selection.to_range(); + let cursor = self.selection.cursor; + let len = part.as_str().len(); + let multi_line = self.wrap; + let selection = self.selection.to_range(); let have_sel = selection.end > selection.start; let string; - if self.text_is_rtl() { + if part.text_is_rtl() { match cmd { Command::Left => cmd = Command::Right, Command::Right => cmd = Command::Left, @@ -1294,7 +1284,7 @@ impl Part { Action::Move(selection.start, None) } Command::Left if cursor > 0 => GraphemeCursor::new(cursor, len, true) - .prev_boundary(self.as_str(), 0) + .prev_boundary(part.as_str(), 0) .unwrap() .map(|index| Action::Move(index, None)) .unwrap_or(Action::None), @@ -1302,14 +1292,14 @@ impl Part { Action::Move(selection.end, None) } Command::Right if cursor < len => GraphemeCursor::new(cursor, len, true) - .next_boundary(self.as_str(), 0) + .next_boundary(part.as_str(), 0) .unwrap() .map(|index| Action::Move(index, None)) .unwrap_or(Action::None), Command::WordLeft if cursor > 0 => { - let mut iter = self.as_str()[0..cursor].split_word_bound_indices(); + let mut iter = part.as_str()[0..cursor].split_word_bound_indices(); let mut p = iter.next_back().map(|(index, _)| index).unwrap_or(0); - while self.as_str()[p..] + while part.as_str()[p..] .chars() .next() .map(|c| c.is_whitespace()) @@ -1324,9 +1314,9 @@ impl Part { Action::Move(p, None) } Command::WordRight if cursor < len => { - let mut iter = self.as_str()[cursor..].split_word_bound_indices().skip(1); + let mut iter = part.as_str()[cursor..].split_word_bound_indices().skip(1); let mut p = iter.next().map(|(index, _)| cursor + index).unwrap_or(len); - while self.as_str()[p..] + while part.as_str()[p..] .chars() .next() .map(|c| c.is_whitespace()) @@ -1343,16 +1333,16 @@ impl Part { // Avoid use of unused navigation keys (e.g. by ScrollComponent): Command::Left | Command::Right | Command::WordLeft | Command::WordRight => Action::None, Command::Up | Command::Down if multi_line => { - let x = match common.edit_x_coord { + let x = match self.edit_x_coord { Some(x) => x, - None => self + None => part .display .text_glyph_pos(cursor) .next_back() .map(|r| r.pos.0) .unwrap_or(0.0), }; - let mut line = self.display.find_line(cursor).map(|r| r.0).unwrap_or(0); + let mut line = part.display.find_line(cursor).map(|r| r.0).unwrap_or(0); // We can tolerate invalid line numbers here! line = match cmd { Command::Up => line.wrapping_sub(1), @@ -1364,13 +1354,13 @@ impl Part { 0..=HALF => len, _ => 0, }; - self.display + part.display .line_index_nearest(line, x) .map(|index| Action::Move(index, Some(x))) .unwrap_or(Action::Move(nearest_end, None)) } Command::Home if cursor > 0 => { - let index = self + let index = part .display .find_line(cursor) .map(|r| r.1.start) @@ -1378,7 +1368,7 @@ impl Part { Action::Move(index, None) } Command::End if cursor < len => { - let index = self + let index = part .display .find_line(cursor) .map(|r| r.1.end) @@ -1390,18 +1380,18 @@ impl Part { // Avoid use of unused navigation keys (e.g. by ScrollComponent): Command::Home | Command::End | Command::DocHome | Command::DocEnd => Action::None, Command::PageUp | Command::PageDown if multi_line => { - let mut v = self + let mut v = part .display .text_glyph_pos(cursor) .next_back() .map(|r| r.pos.into()) .unwrap_or(Vec2::ZERO); - if let Some(x) = common.edit_x_coord { + if let Some(x) = self.edit_x_coord { v.0 = x; } // TODO: page height should be an input? - let mut line_height = common.dpem; - if let Some(line) = self.display.lines().next() { + let mut line_height = self.dpem; + if let Some(line) = part.display.lines().next() { line_height = line.bottom() - line.top(); } let mut h_dist = line_height * 10.0; @@ -1409,23 +1399,23 @@ impl Part { h_dist *= -1.0; } v.1 += h_dist; - Action::Move(self.display.text_index_nearest(v.into()), Some(v.0)) + Action::Move(part.display.text_index_nearest(v.into()), Some(v.0)) } Command::Delete | Command::DelBack if editable && have_sel => { Action::Delete(selection.clone(), EditOp::Delete) } Command::Delete if editable => GraphemeCursor::new(cursor, len, true) - .next_boundary(self.as_str(), 0) + .next_boundary(part.as_str(), 0) .unwrap() .map(|next| Action::Delete(cursor..next, EditOp::Delete)) .unwrap_or(Action::None), Command::DelBack if editable => GraphemeCursor::new(cursor, len, true) - .prev_boundary(self.as_str(), 0) + .prev_boundary(part.as_str(), 0) .unwrap() .map(|prev| Action::Delete(prev..cursor, EditOp::Delete)) .unwrap_or(Action::None), Command::DelWord if editable => { - let next = self.as_str()[cursor..] + let next = part.as_str()[cursor..] .split_word_bound_indices() .nth(1) .map(|(index, _)| cursor + index) @@ -1433,7 +1423,7 @@ impl Part { Action::Delete(cursor..next, EditOp::Delete) } Command::DelWordBack if editable => { - let prev = self.as_str()[0..cursor] + let prev = part.as_str()[0..cursor] .split_word_bound_indices() .next_back() .map(|(index, _)| index) @@ -1441,21 +1431,21 @@ impl Part { Action::Delete(prev..cursor, EditOp::Delete) } Command::SelectAll => { - common.selection.anchor = 0; + self.selection.anchor = 0; shift = true; // hack Action::Move(len, None) } Command::Cut if editable && have_sel => { - cx.set_clipboard((self.as_str()[selection.clone()]).into()); + cx.set_clipboard((part.as_str()[selection.clone()]).into()); Action::Delete(selection.clone(), EditOp::Clipboard) } Command::Copy if have_sel => { - cx.set_clipboard((self.as_str()[selection.clone()]).into()); + cx.set_clipboard((part.as_str()[selection.clone()]).into()); Action::None } Command::Paste if editable => { if let Some(content) = cx.get_clipboard() { - let range = self.trim_paste(common.wrap, &content); + let range = part.trim_paste(self.wrap, &content); string = content; Action::Insert(&string[range], EditOp::Clipboard) } else { @@ -1469,11 +1459,11 @@ impl Part { // We can receive some commands without key focus as a result of // selection focus. Request focus on edit actions (like Command::Cut). if !matches!(action, Action::None | Action::Deselect) { - common.request_key_focus(cx, FocusSource::Synthetic); + self.request_key_focus(cx, FocusSource::Synthetic); } if !matches!(action, Action::None) { - self.cancel_selection_and_ime(common, cx); + self.cancel_selection_and_ime(part, cx); } let edit_op = match action { @@ -1482,12 +1472,12 @@ impl Part { Action::Activate | Action::UndoRedo(_) => None, Action::Insert(_, edit) | Action::Delete(_, edit) => Some(edit), }; - common.save_undo_state(self, edit_op); + self.save_undo_state(part, edit_op); let action = match action { Action::None => unreachable!(), Action::Deselect => { - common.selection.clear_selection(); + self.selection.clear_selection(); cx.redraw(); EventAction::Cursor } @@ -1500,36 +1490,36 @@ impl Part { } else { index..index }; - self.replace_range(range, s); - common.selection.set_position(index + s.len()); - common.edit_x_coord = None; + part.replace_range(range, s); + self.selection.set_position(index + s.len()); + self.edit_x_coord = None; EventAction::Edit } Action::Delete(sel, _) => { - self.replace_range(sel.clone(), ""); - common.selection.set_position(sel.start); - common.edit_x_coord = None; + part.replace_range(sel.clone(), ""); + self.selection.set_position(sel.start); + self.edit_x_coord = None; EventAction::Edit } Action::Move(index, x_coord) => { - common.selection.cursor = index; + self.selection.cursor = index; if !shift { - common.selection.clear_selection(); + self.selection.clear_selection(); } else { - common.set_primary(self, cx); + self.set_primary(part, cx); } - common.edit_x_coord = x_coord; + self.edit_x_coord = x_coord; cx.redraw(); EventAction::Cursor } Action::UndoRedo(redo) => { - if let Some((text, cursor)) = common.undo_stack.undo_or_redo(redo) { - if self.text.as_str() != text { - self.text = text.clone(); - self.status = Status::New; - common.edit_x_coord = None; + if let Some((text, cursor)) = self.undo_stack.undo_or_redo(redo) { + if part.text.as_str() != text { + part.text = text.clone(); + part.status = Status::New; + self.edit_x_coord = None; } - common.selection = *cursor; + self.selection = *cursor; EventAction::Edit } else { EventAction::Used @@ -1539,9 +1529,7 @@ impl Part { Ok(action) } -} -impl Common { /// Set primary clipboard (mouse buffer) contents from selection fn set_primary(&self, part: &Part, cx: &mut EventCx) { if self.has_key_focus && !self.selection.is_empty() && cx.has_primary() { @@ -1646,7 +1634,7 @@ impl Editor { return; // no change } - self.part.cancel_selection_and_ime(&mut self.common, cx); + self.common.cancel_selection_and_ime(&mut self.part, cx); self.part.text = text; self.part.require_reprepare(); @@ -1663,7 +1651,7 @@ impl Editor { /// guard. #[inline] pub fn replace_selected_text(&mut self, cx: &mut EventState, text: &str) { - self.part.cancel_selection_and_ime(&mut self.common, cx); + self.common.cancel_selection_and_ime(&mut self.part, cx); let selection = self.common.selection.to_range(); self.part.replace_range(selection.clone(), text);