Skip to content

fix(ListViewTransitions): null transitions when duration truncates to 0ms - #2791

Merged
bbedward merged 1 commit into
duxlabs-dev:masterfrom
14Do:fix/listview-zero-duration-transitions
Jul 9, 2026
Merged

bbedward merged 1 commit into
duxlabs-dev:masterfrom
14Do:fix/listview-zero-duration-transitions

Conversation

@14Do

@14Do 14Do commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Description

ListViewTransitions hands every DankListView a set of add/remove/displaced/move Transitions whose durations come from Theme.expressiveDurations.*. When the effective duration is 0ms, the transitions still engage ListView's ViewTransition machinery but resolve within the same frame, so removed delegates are never released and displaced items are never repositioned. On a filtered ScriptModel that re-filters on every keystroke, this leaves stale rows and empty gaps behind - reproducible in any DankListView-backed list that filters or reorders.

It happens in two cases:

  • animation speed None (base duration 0), and
  • a Custom duration of 1-2ms.

The second case comes from how the durations are derived. In Theme.qml:

expressiveDurations.fast:             base * 0.4   // used by remove
expressiveDurations.expressiveEffects: base * 0.4  // used by add
expressiveDurations.normal:           base * 0.8   // used by displaced / move

So the shortest sub-duration (remove/add) is base * 0.4, and it is coerced to an int for the animation duration, which truncates toward zero. 1*0.4=0.4 -> 0, 2*0.4=0.8 -> 0, and only 3*0.4=1.2 -> 1 survives. So the cliff is base < 3:

animationSpeed base remove/add = floor(base*0.4) displaced/move = floor(base*0.8) result
0 (None) 0 0 broken
1 0 0 broken
2 0 1 broken
3 1 2 ok
250 (Short preset) 100 200 ok

Presets (250 / 500 / 750) sit well above the cliff, which is why this only shows up with animations disabled or a tiny custom value. Note base 2: displaced is already 1ms but remove is still 0ms and it is still broken, which pins the stale-delegate half specifically on the remove transition.

Fix

Gate the four transitions to null whenever the shortest sub-duration would truncate to 0ms, so the view takes the correct instant path instead of a broken 0ms Transition. base >= 3 is unchanged. The invariant: a view transition should never be a 0ms Transition object - it is either null (instant, correct) or >= 1ms (runs correctly).

Because DankListView already reads ListViewTransitions.* for its defaults, this fixes every consumer at once. The per-call-site add/remove/displaced/move: null overrides added in #2315 (ProcessesView.qml, ClipboardContent.qml) become redundant and could be reverted in a follow-up to restore their fade animations for users who do have animations enabled - left out of this PR to keep it minimal.

Type of change

  • Bug fix (non-breaking change that fixes an issue)

Related issues

None. Generalizes the per-call-site fix from #2315.

Screenshots / video

Minimal repro: two DankListViews over identical data / delegate / ScriptModel, differing only in the transitions. The left uses the shipped ListViewTransitions.*; the right uses the proposed gate (null when the effective duration is 0ms).

  • Screenshot: with animation speed None, the left pane shows stale rows and gaps while filtering, the right pane stays clean.
  • Video: sweeping the Custom animation duration through 0, 1, 2, 3ms - the left pane is broken at 0/1/2 and clean at 3, the right pane is clean throughout and animates from 3.

Interactive repro plugin: https://github.com/14Do/dms-listview-repro

listview-repro-demo.mp4

Checklist

  • My code follows the conventions in CONTRIBUTING.md
  • I have tested my changes locally
  • New user-facing strings are wrapped in I18n.tr() with translator context - N/A, no new strings
  • Go changes - N/A
  • QML changes: ran make lint-qml with no new warnings
  • dlx-docs PR - N/A, no new user-facing behavior

… 0ms

A zero-duration ViewTransition still engages ListView's transition
machinery but resolves within the same frame, so removed delegates are
never released and displaced items are never repositioned. On a filtered
ScriptModel that re-filters on every keystroke this leaves stale rows and
empty gaps behind, visible in any DankListView-backed list.

The shortest sub-duration (remove/add = expressiveDurations.fast =
base * 0.4) is coerced to an int, so it truncates to 0ms for any
animation base duration below 3: animation speed None (0), or a Custom
duration of 1-2ms (1*0.4=0.4->0, 2*0.4=0.8->0, only 3*0.4=1.2->1
survives). Presets (250/500/750) are unaffected, which is why this only
reproduces with animations disabled or a very small custom value.

Gate the four transitions to null whenever that shortest sub-duration
would truncate to 0, so the view takes the correct instant path instead
of a broken 0ms transition. Base >= 3 is unchanged.
@14Do
14Do marked this pull request as ready for review July 9, 2026 15:43
@bbedward
bbedward merged commit ce1595d into duxlabs-dev:master Jul 9, 2026
1 check passed
@14Do
14Do deleted the fix/listview-zero-duration-transitions branch July 10, 2026 09:13
purian23 pushed a commit that referenced this pull request Jul 10, 2026
… 0ms (#2791)

A zero-duration ViewTransition still engages ListView's transition
machinery but resolves within the same frame, so removed delegates are
never released and displaced items are never repositioned. On a filtered
ScriptModel that re-filters on every keystroke this leaves stale rows and
empty gaps behind, visible in any DankListView-backed list.

The shortest sub-duration (remove/add = expressiveDurations.fast =
base * 0.4) is coerced to an int, so it truncates to 0ms for any
animation base duration below 3: animation speed None (0), or a Custom
duration of 1-2ms (1*0.4=0.4->0, 2*0.4=0.8->0, only 3*0.4=1.2->1
survives). Presets (250/500/750) are unaffected, which is why this only
reproduces with animations disabled or a very small custom value.

Gate the four transitions to null whenever that shortest sub-duration
would truncate to 0, so the view takes the correct instant path instead
of a broken 0ms transition. Base >= 3 is unchanged.

(cherry picked from commit ce1595d)
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