use crate::prelude::*; use std::time::{Duration, Instant}; /// What focusing a text field takes from whichever backend is running -- /// tracked here rather than duplicated per backend, since `Selector` and /// `Selectable` (below) are the *only* thing that decides which `TextEdit` /// is the IME's target, and both platforms need the same double-click /// timing and the same "remember which one" bookkeeping. What differs is /// what happens *after* the focus record is set: winit tells the /// compositor an IME area (`focus_gained`, in `default/attr.rs`); on /// android-view a keyboard has to be asked for explicitly, and only from a /// JNI call this crate cannot make outside a view callback -- so /// `focus_gained` there (`android/attr.rs`) just raises a flag the next /// touch callback consumes. See RUST.md's I2. pub trait FocusHost { /// True on a click close enough in time to the previous one to grow a /// selection instead of starting a new one, updating the clock as a /// side effect the way a real double-click timer does. fn recent_click(&mut self) -> bool; fn set_focus(&mut self, id: Option>); /// Called on every tap that should put the IME on `id`: the tap that /// *makes* a `TextEdit` the focus target, and any later tap on one that /// already is. `region` is where it was hit (`None` when the widget /// could not be located, which happens for one it was just deselected /// from). Implementations must be idempotent -- both backends' calls /// (`showSoftInput`, `set_ime_cursor_area`) already are, which is what /// lets the repeat tap be handled by the same call rather than by a /// second "re-show" entry point beside it. fn focus_gained(&mut self, region: Option); /// Whether `id` is the current focus target -- what [`select`] uses to /// tell a fresh press (which must wait to see whether it becomes a tap /// or a drag before focusing/showing the IME, Iris 2026-09-06: "if I /// swipe over the input bar it brings up the keyboard") from a drag /// continuing inside a field that was already focused (an ordinary /// drag-to-select, unaffected). fn is_focused(&self, id: WeakWidget) -> bool; } /// Helper shared by every `FocusHost` impl, so the double-click window is /// one constant rather than one per backend. pub fn recent_click(last_click: &mut Instant) -> bool { let now = Instant::now(); let recent = (now - *last_click) < Duration::from_millis(300); *last_click = now; recent } /// `PressStart`/`Pressing`/`PressEnd`, all for the left button -- what /// [`Selector`]/[`Selectable`] register instead of [`CursorSense:: /// click_or_drag`], so their shared handler (`on_press`, below) sees every /// frame of a gesture and can tell a completed tap from a drag itself, /// rather than reacting to `PressStart` alone the way `click_or_drag`'s /// consumer used to (Iris, 2026-09-06: "if I swipe over the input bar it /// brings up the keyboard"). /// `CursorSense::Cancel` is in the set for the same reason `DragGesture` /// registers it: if a scroll area or a list takes the pointer mid-gesture, /// this field sees no `PressEnd`, and a `press_origin` left set is then /// compared against the *next* press -- a stray selection, or a keyboard /// summoned by a tap somewhere else entirely. fn press_track() -> CursorSenses { CursorSense::click() | CursorSense::Pressing(CursorButton::Left) | CursorSense::unclick() | CursorSense::Cancel } pub struct Selector; impl WidgetAttr for Selector where Rsc::State: FocusHost, { type Input = WeakWidget; fn run(rsc: &mut Rsc, container: WeakWidget, id: Self::Input) { rsc.register_event(container, press_track(), move |ctx, rsc| { let region = ctx.data.render.window_region(&id, &*rsc).unwrap(); let id_pos = region.top_left; let container_pos = ctx .data .render .window_region(&container, &*rsc) .unwrap() .top_left; let pos = ctx.data.pos + container_pos - id_pos; let size = region.size(); on_press( rsc, ctx.data.render, ctx.state, id, pos, size, ctx.data.sense, ); }); } } pub struct Selectable; impl WidgetAttr for Selectable where Rsc::State: FocusHost, { type Input = (); fn run(rsc: &mut Rsc, id: WeakWidget, _: Self::Input) { rsc.register_event(id, press_track(), move |ctx, rsc| { on_press( rsc, ctx.data.render, ctx.state, id, ctx.data.pos, ctx.data.size, ctx.data.sense, ); }); } } /// One press-track frame (`PressStart`, `Pressing` or `PressEnd`) over a /// selectable field. A field that is *already* focused behaves exactly as /// `click_or_drag` always did -- every frame updates the selection, which /// is what lets a finger already inside a focused field drag out a /// selection. A field that is **not** focused withholds `select`'s /// focus-granting side effects (and so the platform-specific `focus_gained` /// that shows the keyboard) until the press resolves as a tap: `PressEnd` /// with no frame in between having moved past [`DRAG_SLOP`] from where the /// press began. A drag recognised before release simply cancels the /// pending tap and does nothing further here -- it is not consumed, so /// whatever is behind the field (a list to pan) still sees every frame of /// it, the same as a drag that never touched a selectable field at all. fn on_press( rsc: &mut impl UiRsc, render: &UiRenderState, state: &mut impl FocusHost, id: WeakWidget, pos: Vec2, size: Vec2, sense: CursorSense, ) { if state.is_focused(id) { // Already focused, so there is no keyboard to withhold -- but a // vertical drag still is not a selection. Android's own `EditText` // scrolls its overflowed text on a vertical drag and starts a // selection only from a long press; a scroll area wrapping this // field (`ScrollController::drag`) is what actually pans, and it needs the // first frames of the gesture not to have selected anything behind // it before it crosses `DRAG_SLOP` and takes pointer capture. // `press_origin` carries the same meaning here as in the unfocused // branch below -- "this gesture is still eligible", cleared the // moment it becomes a drag -- so there is one flag, not two. match sense { CursorSense::PressStart(_) => { let recent = state.recent_click(); id.edit(rsc).text.press_origin = Some(pos); id.edit(rsc).select(pos, size, false, recent); } CursorSense::Pressing(_) | CursorSense::PressEnd(_) => { let mut ctx = id.edit(rsc); let Some(origin) = ctx.text.press_origin else { return; }; let (dx, dy) = (pos.x - origin.x, pos.y - origin.y); if dy.abs() > DRAG_SLOP && dy.abs() >= dx.abs() { ctx.text.press_origin = None; return; } let ended = matches!(sense, CursorSense::PressEnd(_)); if ended { ctx.text.press_origin = None; } ctx.select(pos, size, true, false); // A tap on a field that is *already* focused asks for the // keyboard again (Iris's phone, 2026-09-06: "I can't reopen // keyboard by tapping on message box after it already // happened once"). Dismissing the IME -- back gesture, or // its own hide button -- takes the keyboard away but leaves // the field focused, so without this the one branch that // requests it (the unfocused one below) never runs again // and the field is permanently unable to summon it. // Android's own `EditText` does exactly this: every tap on // a focused field calls `showSoftInput`, which is a no-op // when the keyboard is already up. // // Gated on the same tap-vs-drag test the unfocused branch // uses, not on `PressEnd` alone, so a drag-to-select that // happens to finish inside the field does not summon a // keyboard the reader was not asking for. if ended && dx.abs() <= DRAG_SLOP && dy.abs() <= DRAG_SLOP { state.focus_gained(render.window_region(&id, &*rsc)); } } CursorSense::Cancel => id.edit(rsc).text.press_origin = None, _ => {} } return; } match sense { CursorSense::PressStart(_) => { id.edit(rsc).text.press_origin = Some(pos); } CursorSense::Pressing(_) => { let ctx = id.edit(rsc); if let Some(origin) = ctx.text.press_origin && ((pos.x - origin.x).abs() > DRAG_SLOP || (pos.y - origin.y).abs() > DRAG_SLOP) { // Past the slop before release: this is a drag, not a tap // -- give up the pending focus rather than granting it once // the finger lifts wherever it happens to be by then. ctx.text.press_origin = None; } } // The gesture was taken by somebody else, so it is not a tap and // must not grant focus when it ends out of this widget's sight. CursorSense::Cancel => id.edit(rsc).text.press_origin = None, CursorSense::PressEnd(_) => { let was_tap = id.edit(rsc).text.press_origin.take().is_some(); if was_tap { let recent = state.recent_click(); id.edit(rsc).select(pos, size, false, recent); state.set_focus(Some(id)); state.focus_gained(render.window_region(&id, &*rsc)); } } _ => {} } }