use super::*;
use crate::event::EditingState;
struct ImeWidget;
impl crate::widget::Widget for ImeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 100.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Pointer(p) = event {
if p.phase == PointerPhase::Down && p.position.x < 50.0 {
ctx.request_focus();
ctx.publish_ime_state(ImeState {
active: true,
editing: EditingState {
text: "abc".to_string(),
selection_base: 3,
selection_extent: 3,
composing_base: -1,
composing_extent: -1,
},
caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
content_type: Default::default(),
suppress_soft_keyboard: false,
});
return EventResult::Handled;
}
if p.phase == PointerPhase::Down {
return EventResult::Handled;
}
}
EventResult::Ignored
}
}
struct ImeView;
impl View<ClickState> for ImeView {
type Element = ImeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
ImeWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut ImeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
fn ime_logic(_state: &mut ClickState) -> ImeView {
ImeView
}
#[test]
fn focus_and_ime_state_surface_and_clear_on_blur() {
let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut ime_logic, &mut state);
root.layout(Size::new(100.0, 100.0));
assert!(!root.is_focus_active());
assert!(root.ime_state().is_none());
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
assert!(root.is_focus_active());
let ime = root
.ime_state()
.expect("focused widget published an IME surface");
assert!(ime.active);
assert_eq!(ime.editing.text, "abc");
root.rebuild(&mut ime_logic, &mut state);
assert!(root.ime_state().is_some());
root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
assert!(!root.is_focus_active());
assert!(root.ime_state().is_none());
}
fn cleared_surface() -> ImeState {
ImeState {
active: false,
editing: EditingState {
text: String::new(),
selection_base: -1,
selection_extent: -1,
composing_base: -1,
composing_extent: -1,
},
caret: None,
content_type: Default::default(),
suppress_soft_keyboard: false,
}
}
struct PopImeWidget {
clear: Rc<Cell<bool>>,
}
impl crate::widget::Widget for PopImeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 100.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
if self.clear.get() {
ctx.publish_ime_state(cleared_surface());
} else if ctx.has_focus() {
ctx.publish_ime_state(PaintImeWidget::surface("abc"));
}
}
fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
ctx.request_focus();
ctx.publish_ime_state(PaintImeWidget::surface("abc"));
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
struct PopImeView {
clear: Rc<Cell<bool>>,
}
impl View<ClickState> for PopImeView {
type Element = PopImeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> PopImeWidget {
PopImeWidget {
clear: self.clear.clone(),
}
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut PopImeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
#[test]
fn inactive_paint_publish_releases_the_whole_session() {
let clear = Rc::new(Cell::new(false));
let mut build = {
let clear = clear.clone();
move |_state: &mut ClickState| PopImeView {
clear: clear.clone(),
}
};
let mut root: RenderRoot<ClickState, PopImeView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut build, &mut state);
root.layout(Size::new(100.0, 100.0));
let mut scene = RecordingScene::default();
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
root.paint(&mut scene, FrameTime::ZERO);
assert!(root.is_focus_active());
assert!(root.ime_state().is_some_and(|s| s.active));
let focused = root.focus_ime_generation();
clear.set(true);
root.paint(&mut scene, FrameTime::ZERO);
assert!(
!root.is_focus_active(),
"an inactive publish ends the session, not just the surface"
);
assert_eq!(
root.ime_state(),
None,
"the surface is dropped, never parked at Some(inactive)"
);
assert_eq!(
root.focus_ime_generation(),
focused.wrapping_add(1),
"one release is exactly one edge"
);
let released = root.focus_ime_generation();
for _ in 0..30 {
root.paint(&mut scene, FrameTime::ZERO);
}
assert!(!root.is_focus_active());
assert!(root.ime_state().is_none());
assert_eq!(
root.focus_ime_generation(),
released,
"an inactive publish against an already-released root is inert"
);
}
struct UnmountView {
orphan: Rc<Cell<bool>>,
}
impl View<ClickState> for UnmountView {
type Element = ImeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeWidget {
ImeWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut ImeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
if self.orphan.get() {
crate::event::mark_focus_orphaned();
return ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
ChangeFlags::NONE
}
}
#[test]
fn generic_unmount_orphan_releases_the_whole_session() {
let _ = crate::event::take_focus_orphaned();
let orphan = Rc::new(Cell::new(false));
let mut build = {
let orphan = orphan.clone();
move |_state: &mut ClickState| UnmountView {
orphan: orphan.clone(),
}
};
let mut root: RenderRoot<ClickState, UnmountView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut build, &mut state);
root.layout(Size::new(100.0, 100.0));
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
assert!(root.is_focus_active());
assert!(root.ime_state().is_some());
let focused = root.focus_ime_generation();
orphan.set(true);
root.rebuild(&mut build, &mut state);
assert!(
!root.is_focus_active(),
"the root's focus mirror does not outlive the widget it mirrors"
);
assert!(root.ime_state().is_none());
assert_eq!(
root.focus_ime_generation(),
focused.wrapping_add(1),
"one orphaned focus path is exactly one edge"
);
let released = root.focus_ime_generation();
orphan.set(false);
root.rebuild(&mut build, &mut state);
assert_eq!(root.focus_ime_generation(), released);
orphan.set(true);
root.rebuild(&mut build, &mut state);
assert_eq!(root.focus_ime_generation(), released);
assert!(!root.is_focus_active());
}
struct PaintImeWidget {
published: Rc<RefCell<String>>,
}
impl PaintImeWidget {
fn surface(text: &str) -> ImeState {
ImeState {
active: true,
editing: EditingState {
text: text.to_string(),
selection_base: 0,
selection_extent: 0,
composing_base: -1,
composing_extent: -1,
},
caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
content_type: Default::default(),
suppress_soft_keyboard: false,
}
}
}
impl crate::widget::Widget for PaintImeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 100.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
ctx.publish_ime_state(Self::surface(&self.published.borrow()));
}
fn event(&mut self, ctx: &mut crate::event::EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
ctx.request_focus();
EventResult::Handled
}
InputEvent::Key(_) => {
ctx.release_focus();
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
struct PaintImeView {
published: Rc<RefCell<String>>,
}
impl View<ClickState> for PaintImeView {
type Element = PaintImeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> PaintImeWidget {
PaintImeWidget {
published: self.published.clone(),
}
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut PaintImeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
fn key_event() -> InputEvent {
InputEvent::Key(crate::event::KeyEvent {
key: crate::event::Key::Named(crate::event::NamedKey::Enter),
modifiers: crate::event::Modifiers::default(),
repeat: false,
})
}
#[test]
fn focus_ime_generation_moves_on_every_pointer_transition_only() {
let mut root: RenderRoot<ClickState, ImeView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut ime_logic, &mut state);
root.layout(Size::new(100.0, 100.0));
let idle = root.focus_ime_generation();
root.rebuild(&mut ime_logic, &mut state);
assert_eq!(
root.focus_ime_generation(),
idle,
"a rebuild is not an edge"
);
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
let focused = root.focus_ime_generation();
assert_ne!(
focused, idle,
"focus + IME publish must move the generation"
);
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
assert_eq!(
root.focus_ime_generation(),
focused,
"a same-value focus/IME write must not spin the edge"
);
root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
let blurred = root.focus_ime_generation();
assert_ne!(blurred, focused, "a blur must move the generation");
root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 20.0));
assert_eq!(
root.focus_ime_generation(),
blurred,
"blurring an already-blurred root must not move the generation"
);
}
#[test]
fn focus_ime_generation_ignores_an_unchanged_paint_republish() {
let published = Rc::new(RefCell::new("abc".to_string()));
let mut build = {
let published = published.clone();
move |_state: &mut ClickState| PaintImeView {
published: published.clone(),
}
};
let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut build, &mut state);
root.layout(Size::new(100.0, 100.0));
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
let steady = root.focus_ime_generation();
assert_eq!(
root.ime_state(),
Some(PaintImeWidget::surface("abc")),
"the paint pass published the focused widget's surface"
);
for _ in 0..120 {
root.paint(&mut scene, FrameTime::ZERO);
}
assert_eq!(
root.focus_ime_generation(),
steady,
"an unchanged paint republish must never move the generation"
);
*published.borrow_mut() = "abcd".to_string();
root.paint(&mut scene, FrameTime::ZERO);
let edited = root.focus_ime_generation();
assert_ne!(edited, steady, "a changed republish IS an edge");
for _ in 0..10 {
root.paint(&mut scene, FrameTime::ZERO);
}
assert_eq!(
root.focus_ime_generation(),
edited,
"the session goes quiet again at the new value"
);
}
#[test]
fn focus_ime_generation_moves_on_a_focus_release_only_once() {
let published = Rc::new(RefCell::new("abc".to_string()));
let mut build = {
let published = published.clone();
move |_state: &mut ClickState| PaintImeView {
published: published.clone(),
}
};
let mut root: RenderRoot<ClickState, PaintImeView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut build, &mut state);
root.layout(Size::new(100.0, 100.0));
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
let focused = root.focus_ime_generation();
assert!(root.is_focus_active());
root.event(&mut state, &key_event());
let released = root.focus_ime_generation();
assert!(!root.is_focus_active());
assert!(root.ime_state().is_none());
assert_ne!(
released, focused,
"a focus release must move the generation"
);
root.event(&mut state, &key_event());
root.paint(&mut scene, FrameTime::ZERO);
assert_eq!(
root.focus_ime_generation(),
released,
"releasing an already-released focus must not move the generation"
);
}
const SHARED_FIELD_TEXT: &str = "shared";
struct SessionField {
name: &'static str,
publishes: bool,
}
impl SessionField {
fn surface(text: &str) -> ImeState {
ImeState {
active: true,
editing: EditingState {
text: text.to_string(),
selection_base: 0,
selection_extent: 0,
composing_base: -1,
composing_extent: -1,
},
caret: Some(kurbo::Rect::new(0.0, 0.0, 1.0, 12.0)),
content_type: Default::default(),
suppress_soft_keyboard: false,
}
}
}
impl crate::widget::Widget for SessionField {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 20.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
ctx.request_focus();
if self.publishes {
ctx.publish_ime_state(Self::surface(SHARED_FIELD_TEXT));
}
EventResult::Handled
}
InputEvent::Ime(_) => {
ctx.publish_ime_state(Self::surface(self.name));
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
struct TwoFields {
top: crate::widget::ChildPod,
bottom: crate::widget::ChildPod,
}
impl crate::widget::Widget for TwoFields {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.top.layout_child(ctx, bc);
self.top.set_origin(Point::new(0.0, 0.0));
self.bottom.layout_child(ctx, bc);
self.bottom.set_origin(Point::new(0.0, 50.0));
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.top.paint_child(ctx, scene);
self.bottom.paint_child(ctx, scene);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if matches!(event, InputEvent::Pointer(_)) {
let pos = event.position();
if self.top.contains(pos) {
return self.top.event_child(ctx, event);
}
if self.bottom.contains(pos) {
return self.bottom.event_child(ctx, event);
}
return EventResult::Ignored;
}
if self.top.holds_live_focus() {
return self.top.event_child(ctx, event);
}
if self.bottom.holds_live_focus() {
return self.bottom.event_child(ctx, event);
}
EventResult::Ignored
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.top.semantics_child(ctx);
self.bottom.semantics_child(ctx);
}
}
struct TwoFieldsView {
bottom_publishes: bool,
}
impl View<ClickState> for TwoFieldsView {
type Element = TwoFields;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> TwoFields {
TwoFields {
top: crate::widget::ChildPod::new(Box::new(SessionField {
name: "top",
publishes: true,
})),
bottom: crate::widget::ChildPod::new(Box::new(SessionField {
name: "bottom",
publishes: self.bottom_publishes,
})),
}
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut TwoFields,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
fn edit_event() -> InputEvent {
InputEvent::Ime(crate::event::ImeEvent::ApplyEditingState(
SessionField::surface(SHARED_FIELD_TEXT).editing,
))
}
fn two_fields_focused(
bottom_publishes: bool,
) -> (RenderRoot<ClickState, TwoFieldsView>, ClickState) {
let mut root: RenderRoot<ClickState, TwoFieldsView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut |_| TwoFieldsView { bottom_publishes }, &mut state);
root.layout(Size::new(200.0, 200.0));
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
assert!(root.is_focus_active(), "the top field opened a session");
(root, state)
}
#[test]
fn focus_epoch_moves_when_focus_crosses_two_fields_publishing_alike() {
let (mut root, mut state) = two_fields_focused(true);
let first_session = root.focus_epoch();
let steady_edge = root.focus_ime_generation();
assert_eq!(
root.ime_state(),
Some(SessionField::surface(SHARED_FIELD_TEXT))
);
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
assert!(root.is_focus_active());
assert_eq!(
root.focus_ime_generation(),
steady_edge,
"the edge generation is blind to this move, which is why a caller \
asking 'is this the same session?' must not be built on it"
);
assert_ne!(
root.focus_epoch(),
first_session,
"the session identity must move when focus crosses to another field"
);
root.event(&mut state, &edit_event());
assert_eq!(
root.ime_state(),
Some(SessionField::surface("bottom")),
"the second field is the one holding the session"
);
}
#[test]
fn focus_epoch_moves_when_the_field_taking_focus_publishes_nothing() {
let (mut root, mut state) = two_fields_focused(false);
let first_session = root.focus_epoch();
let steady_edge = root.focus_ime_generation();
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
assert!(root.is_focus_active());
assert_eq!(root.ime_state(), None, "the left field's surface is gone");
assert_ne!(root.focus_ime_generation(), steady_edge);
assert_ne!(
root.focus_epoch(),
first_session,
"an honoured claim moves the session identity whether or not the \
claimant publishes anything"
);
root.event(&mut state, &edit_event());
assert_eq!(
root.ime_state(),
Some(SessionField::surface("bottom")),
"the second field is the one holding the session"
);
}
#[test]
fn focus_epoch_ignores_an_edit_inside_one_session() {
let (mut root, mut state) = two_fields_focused(true);
let session = root.focus_epoch();
let before_edit = root.focus_ime_generation();
root.event(&mut state, &edit_event());
assert_eq!(
root.ime_state(),
Some(SessionField::surface("top")),
"the focused field applied the edit and republished"
);
assert_ne!(
root.focus_ime_generation(),
before_edit,
"a changed surface IS an edge"
);
assert_eq!(
root.focus_epoch(),
session,
"an edit does not end or restart the session it lands in"
);
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 90.0));
assert!(!root.is_focus_active());
assert_ne!(
root.focus_epoch(),
session,
"a release retires the session's identity"
);
}
#[test]
fn pointer_focus_move_to_a_non_publishing_pod_clears_the_surface_at_paint() {
let (mut root, mut state) = two_fields_focused(false);
let first_session = root.focus_epoch();
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
assert!(root.ime_state().is_some(), "the top field's surface stands");
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
let moved = root.focus_epoch();
assert_ne!(moved, first_session);
root.paint(&mut scene, FrameTime::ZERO);
assert!(root.is_focus_active(), "the bottom pod holds the session");
assert!(root.ime_state().is_none(), "no stale surface after paint");
assert_eq!(root.focus_epoch(), moved, "the epoch moved exactly once");
root.paint(&mut scene, FrameTime::ZERO);
assert!(root.ime_state().is_none());
}
#[test]
fn pointer_focus_move_between_publishing_pods_keeps_the_new_surface() {
let (mut root, mut state) = two_fields_focused(true);
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 60.0));
root.paint(&mut scene, FrameTime::ZERO);
assert!(root.is_focus_active());
assert_eq!(
root.ime_state(),
Some(SessionField::surface(SHARED_FIELD_TEXT))
);
}
#[test]
fn outside_tap_blur_still_clears_the_surface() {
let (mut root, mut state) = two_fields_focused(true);
let mut scene = RecordingScene::default();
root.paint(&mut scene, FrameTime::ZERO);
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 90.0));
root.paint(&mut scene, FrameTime::ZERO);
assert!(!root.is_focus_active());
assert!(root.ime_state().is_none());
}
struct ReclaimField {
downs: u32,
}
impl crate::widget::Widget for ReclaimField {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 20.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
ctx.request_focus();
if self.downs == 0 {
ctx.publish_ime_state(SessionField::surface(SHARED_FIELD_TEXT));
}
self.downs += 1;
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
struct ReclaimRoot {
pod: crate::widget::ChildPod,
}
impl crate::widget::Widget for ReclaimRoot {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.pod.layout_child(ctx, bc);
self.pod.set_origin(Point::new(0.0, 0.0));
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.pod.paint_child(ctx, scene);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
self.pod.event_child(ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.pod.semantics_child(ctx);
}
}
struct ReclaimView;
impl View<ClickState> for ReclaimView {
type Element = ReclaimRoot;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ReclaimRoot {
ReclaimRoot {
pod: crate::widget::ChildPod::new(Box::new(ReclaimField { downs: 0 })),
}
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut ReclaimRoot,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
#[test]
fn a_reclaim_from_the_live_chain_keeps_the_surface() {
let mut root: RenderRoot<ClickState, ReclaimView> = RenderRoot::new();
let mut state = ClickState::default();
root.rebuild(&mut |_| ReclaimView, &mut state);
root.layout(Size::new(200.0, 200.0));
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
let surface = root.ime_state();
assert!(surface.is_some(), "the first press opened the surface");
let edge = root.focus_ime_generation();
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
assert!(root.is_focus_active());
assert_eq!(
root.ime_state(),
surface,
"a live re-claim keeps the surface"
);
assert_eq!(root.focus_ime_generation(), edge, "and fires no IME edge");
}