The field now owns the keyboard; what it does not yet own is the characters

Measured, not reasoned. A per-frame trace of the ImGui state is what finally named the mechanism,
and it is a deadlock, not a focus problem:

  [UX] frame want_text=0 want_kb=0 active=0 buf=158.74   <- frame 1: the widget is not active yet
  [UX] frame want_text=0 want_kb=0 active=1 buf=158.74   <- frame 2: now it is
  (nothing further)                                       <- and the canvas stops

This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a frame,
from the active item; GLCanvas3D::on_char only calls render() when update_key_data() says ImGui
wants it. So: no frames -> WantTextInput never turns on -> no render on a keystroke -> still no
frames. The characters sit in ImGui's input queue and the field is exactly as deaf as the window
it replaced, for a completely different reason.

request_frame breaks the circle, and the same trace says so:

  [UX] frame want_text=1 want_kb=1 active=1

That is the first time in this file's history that the value field has owned the keyboard without
asking a window manager for it.

STILL OPEN: the typed characters do not reach the buffer (buf stays at the prefill) and the frames
stop after nine. The pump is the suspect — on software GL request_repaint() calls m_canvas->render()
SYNCHRONOUSLY, so this asks for a render from inside a render; it needs to schedule one instead.
That is the next thing to measure, not to guess.

The deployed binary on behemoth is restored to 00d6c191dc (md5 0eeb9a58cef5), byte-identical to
the last good build. Nothing from this branch is installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
This commit is contained in:
Tommaso Bianchi
2026-09-06 10:58:05 +02:00
co-authored by Claude Opus 5
parent 5ab3072b9f
commit d82c8b59c2
3 changed files with 32 additions and 0 deletions
+1
View File
@@ -87,6 +87,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
// The tool draws it: it owns the frame's ImGui pass and the render scale. Handing it a raw
// pointer rather than the unique_ptr keeps the ownership where it was.
m_sketch_tool.inline_editor = m_inline_editor.get();
m_inline_editor->request_frame = [this] { request_repaint(); };
m_sketch_tool.on_inline_edit = [this](wxPoint screen_px, double current,
const std::string& title,
std::function<void(double)> commit,
+16
View File
@@ -170,11 +170,27 @@ bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
ImGuiInputTextFlags_EnterReturnsTrue
| ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_CharsDecimal);
// MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the
// keyboard and whether our widget is the active one. "Typing does not arrive" has two very
// different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never
// processes ImGui's queued characters) versus frames that run while the input is not active —
// and they are indistinguishable from outside.
if (std::getenv("SNAPORCA_UXTRACE")) {
const ImGuiIO& io = ImGui::GetIO();
std::fprintf(stderr, "[UX] frame title=%s want_text=%d want_kb=%d active=%d buf=%s\n",
m_title.c_str(), (int) io.WantTextInput, (int) io.WantCaptureKeyboard,
(int) ImGui::IsItemActive(), m_buf);
std::fflush(stderr);
}
ImGui::PopItemWidth();
imgui.end();
ImGui::PopStyleVar();
ImGuiWrapper::pop_common_window_style();
// Keep the frames coming while the field is up — see request_frame's note in the header.
if (m_open && request_frame)
request_frame();
// Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that
// must not happen inside this frame's window.
if (entered)
+15
View File
@@ -54,6 +54,21 @@ public:
// frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew.
bool render(ImGuiWrapper& imgui, float scale);
// Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock
// that only a per-frame trace shows:
//
// [UX] frame want_text=0 want_kb=0 active=0 <- frame 1: the widget is not active yet
// [UX] frame want_text=0 want_kb=0 active=1 <- frame 2: it is now
// (nothing further) <- the canvas has nothing to redraw, so it stops
//
// This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a
// frame, from the active item, and GLCanvas3D::on_char only calls render() when
// update_key_data() says ImGui wants it. No frames -> WantTextInput never turns on -> no
// render on a keystroke -> still no frames. The characters sit in ImGui's queue and the field
// looks exactly as deaf as the window it replaced. One repaint per frame while it is open
// breaks the circle.
std::function<void()> request_frame;
// Kept because callers ask them, but there is no longer any difference to report: with no
// window there is no state where the field is on screen but logically closed, and no state
// where it is open but somebody else holds the keyboard.