iris: a tap on an empty text field left no caret, so typing was silently dropped

TextEditCtx::select compared the tap against the laid-out text's own box
and cleared the selection for anything outside it. An empty field lays
out to a zero-width box, so tapping the composer granted focus and opened
the keyboard with no caret, and insert_str returns early without one --
every keystroke went nowhere and no glyph was ever emitted. Parley clamps
a point outside the layout by itself, and a press reaching select() has
already been hit-tested to the widget, so there was nothing for the
'outside' branch to mean.

insert_str now debug_asserts rather than dropping input silently, and
UiRenderState::draw_started -- a re-entrancy guard whose test was written
after its own remove(), so it could never fire, and which grew by one
entry per widget ever drawn -- is restored to what it was meant to be:
inserted around Widget::draw, removed when it returns, asserted empty at
the top of every update.
This commit is contained in:
iris committed 2026-09-06 13:43:56 -04:00
1 parent d9872989fa
commit c02152a4f4
8 files changed
+168 -47

No files matched your search

+28 -3
View File
@@ -18,6 +18,18 @@ pub struct UiRenderState {
old_root: Option<WidgetId>,
resized: bool,
/// The widgets whose `Widget::draw` is on the stack right now -- so
/// [`Self::redraw`] can tell "this widget needs drawing again" from
/// "an ancestor is drawing it at this very moment", where a second
/// draw would leave the first one's primitives behind with nothing
/// owning them. An id is inserted immediately before `draw` is called
/// and removed the moment it returns (both in `draw_inner`), so this
/// is empty between frames -- asserted at the end of `update`.
///
/// It used to only ever be inserted into, and `redraw` removed the id
/// *before* testing for it, which made the test constant `false`: the
/// guard could never fire and the set grew by one entry per widget
/// ever drawn and was never emptied.
draw_started: HashSet<WidgetId>,
/// The widget currently holding exclusive pointer input, if any --
@@ -115,6 +127,11 @@ impl UiRenderState {
);
}
let root = root.into();
debug_assert!(
self.draw_started.is_empty(),
"a previous frame left {} widget(s) marked as mid-draw",
self.draw_started.len(),
);
if self.needs_redraw_all(root) {
self.redraw_all(root, rsc);
self.old_root = root.map(|r| r.id());
@@ -213,7 +230,12 @@ impl UiRenderState {
}
// draw widget
self.draw_started.insert(id);
let reentrant = !self.draw_started.insert(id);
debug_assert!(
!reentrant,
"widget {id:?} is being drawn while its own draw is already on the stack; \
the second draw's primitives would orphan the first's"
);
let move_slot = match old_move_slot {
// Reused across a real redraw of the same id: the fresh
@@ -259,6 +281,7 @@ impl UiRenderState {
painter.state.draw_count += 1;
let size = widget.draw(&mut painter);
drop(widget);
painter.state.draw_started.remove(&id);
let Painter {
state: _,
@@ -535,7 +558,10 @@ impl UiRenderState {
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId, rsc: &mut dyn UiRsc) {
rsc.widgets_mut().needs_redraw.remove(&id);
self.draw_started.remove(&id);
// An ancestor is drawing this widget right now, and that draw is
// about to write fresh primitives for it. Drawing it a second time
// here would leave one of the two copies on screen with nothing
// owning it -- see `draw_started`'s own doc.
if self.draw_started.contains(&id) {
return;
}
@@ -561,7 +587,6 @@ impl UiRenderState {
Some(active.move_slot),
rsc,
);
// If this widget's own reported size changed, its parent's layout
// (which placed it using the old size) is now stale and needs to
// relay out too. Checked after the real draw, not before it --