Skip to content

input: Fix Input may infinite repaint loop in occurs at fractional DPI scale factors (e.g. 125%, 150%).#2216

Open
lyl2dora wants to merge 3 commits into
longbridge:mainfrom
lyl2dora:fix/input-paint-perpetual-notify
Open

input: Fix Input may infinite repaint loop in occurs at fractional DPI scale factors (e.g. 125%, 150%).#2216
lyl2dora wants to merge 3 commits into
longbridge:mainfrom
lyl2dora:fix/input-paint-perpetual-notify

Conversation

@lyl2dora

@lyl2dora lyl2dora commented Apr 3, 2026

Copy link
Copy Markdown

Fix an infinite repaint loop (perpetual cx.notify()) in the Input component
that occurs at fractional DPI scale factors (e.g. 125%, 150%).

Root cause: taffy's layout rounding produces sub-pixel floating-point
differences between frames. paint() unconditionally called cx.notify() at
the end, and update_scroll_offset / set_input_bounds also called
cx.notify() without checking whether the value actually changed — creating a
paint → notify → repaint → paint → … infinite loop.

Fix (3 commits):

  1. Add physical-pixel granularity guards to update_scroll_offset and
    set_input_bounds — only cx.notify() when the rounded physical pixel
    value actually changes.
  2. Remove the unconditional cx.notify() at the end of paint(), since the
    two functions above now notify conditionally.
  3. Cap edge_height and top_bottom_margin to viewport height, preventing
    false scroll triggers when taffy rounds bounds.height below line_height.

lyl2dora added 3 commits April 3, 2026 18:38
…input_bounds

Taffy layout rounding can produce sub-pixel floating-point differences
between frames (e.g., 34.5px → 34.33px at fractional scale factors).
When update_scroll_offset and set_input_bounds unconditionally call
cx.notify(), these tiny differences trigger an infinite repaint loop:

  paint → set offset (unchanged at physical pixels) → notify → repaint → …

Fix: snap values to physical-pixel integers (round(value × scale_factor))
before comparing. Only call cx.notify() when the physical pixel value
actually changes. This breaks the perpetual loop while preserving correct
repaints for real scroll/resize changes.
paint() is a read-only observation endpoint in GPUI's render cycle.
Calling cx.notify() unconditionally at the end of paint creates an
infinite repaint loop: paint → notify → repaint → paint → …

The previous commit added conditional notification to set_input_bounds
and update_scroll_offset (only when values actually change at
physical-pixel granularity), so the unconditional cx.notify() at the
end of paint is no longer needed and can be safely removed.
At fractional DPI scale factors (e.g. 1.5×), taffy's edge-rounding can
shrink the reported bounds.height below line_height (e.g. 34.5 physical
pixels → 34 physical pixels). When the scroll margin (edge_height or
top_bottom_margin) exceeds the viewport, every cursor movement falsely
triggers a scroll adjustment, causing visible jitter.

Fix: clamp both margins to bounds.size.height so they can never exceed
the actual viewport, eliminating the false scroll triggers.
@huacnlee huacnlee self-assigned this Apr 3, 2026
@huacnlee

huacnlee commented Apr 3, 2026

Copy link
Copy Markdown
Member

Would you please upload some screenshot for this infinite repaint loop?

@lyl2dora

lyl2dora commented Apr 3, 2026

Copy link
Copy Markdown
Author

Reproduction (Windows 11, 150% DPI scaling)

Modified examples/input to simulate inline rename — a common pattern for file trees / tables:

Input::new(&self.input_state)
.appearance(false)
.px_0()
.py_0()
.h_full()

before.mp4

Each click shifts the text down by ~0.33px (0.5 physical pixel at 1.5×). The root cause:

  1. taffy rounds a fractional physical height (e.g. n.5 → n), making bounds.height slightly less than line_height
  2. scroll_to() uses line_height as edge margin — when margin > viewport, it misjudges the cursor as out of bounds and
    applies a negative scroll offset
  3. paint() unconditionally calls cx.notify(), and update_scroll_offset / set_input_bounds compare floats with !=, so
    sub-pixel noise keeps triggering repaints indefinitely

This affects any Input used for inline editing (zero padding, tight height) at fractional DPI scales (125%, 150%).

@huacnlee huacnlee changed the title Fix/input paint perpetual notify input: Fix an infinite repaint loop, Apr 7, 2026
@huacnlee huacnlee changed the title input: Fix an infinite repaint loop, input: Fix Input may infinite repaint loop in occurs at fractional DPI scale factors (e.g. 125%, 150%). Apr 7, 2026
cx.notify();

let snap = |v: Pixels| (v.as_f32() * scale_factor).round() as i32;
if snap(old.x) != snap(offset.x) || snap(old.y) != snap(offset.y) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conversion process is strange, and I don't think it's a solution to the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants