1- import { Flex } from '@invoke-ai/ui-library' ;
1+ import { Box , Flex , forwardRef } from '@invoke-ai/ui-library' ;
22import { useStore } from '@nanostores/react' ;
3- import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent ' ;
3+ import { logger } from 'app/logging/logger ' ;
44import { useCanvasSessionContext } from 'features/controlLayers/components/SimpleSession/context' ;
55import { QueueItemPreviewMini } from 'features/controlLayers/components/SimpleSession/QueueItemPreviewMini' ;
66import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate' ;
7- import { memo , useEffect } from 'react' ;
7+ import { useOverlayScrollbars } from 'overlayscrollbars-react' ;
8+ import type { CSSProperties , RefObject } from 'react' ;
9+ import { memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
10+ import type { Components , ItemContent , ListRange , VirtuosoHandle , VirtuosoProps } from 'react-virtuoso' ;
11+ import { Virtuoso } from 'react-virtuoso' ;
12+ import type { S } from 'services/api/types' ;
13+
14+ import { getQueueItemElementId } from './shared' ;
15+
16+ const log = logger ( 'system' ) ;
17+
18+ const virtuosoStyles = {
19+ width : '100%' ,
20+ height : '72px' ,
21+ } satisfies CSSProperties ;
22+
23+ type VirtuosoContext = { selectedItemId : number | null } ;
24+
25+ /**
26+ * Scroll the item at the given index into view if it is not currently visible.
27+ */
28+ const scrollIntoView = (
29+ targetIndex : number ,
30+ rootEl : HTMLDivElement ,
31+ virtuosoHandle : VirtuosoHandle ,
32+ range : ListRange
33+ ) => {
34+ if ( range . endIndex === 0 ) {
35+ // No range is rendered; no need to scroll to anything.
36+ return ;
37+ }
38+
39+ const targetItem = rootEl . querySelector ( `#${ getQueueItemElementId ( targetIndex ) } ` ) ;
40+
41+ if ( ! targetItem ) {
42+ if ( targetIndex > range . endIndex ) {
43+ virtuosoHandle . scrollToIndex ( {
44+ index : targetIndex ,
45+ behavior : 'auto' ,
46+ align : 'end' ,
47+ } ) ;
48+ } else if ( targetIndex < range . startIndex ) {
49+ virtuosoHandle . scrollToIndex ( {
50+ index : targetIndex ,
51+ behavior : 'auto' ,
52+ align : 'start' ,
53+ } ) ;
54+ } else {
55+ log . debug (
56+ `Unable to find queue item at index ${ targetIndex } but it is in the rendered range ${ range . startIndex } -${ range . endIndex } `
57+ ) ;
58+ }
59+ return ;
60+ }
61+
62+ // We found the image in the DOM, but it might be in the overscan range - rendered but not in the visible viewport.
63+ // Check if it is in the viewport and scroll if necessary.
64+
65+ const itemRect = targetItem . getBoundingClientRect ( ) ;
66+ const rootRect = rootEl . getBoundingClientRect ( ) ;
67+
68+ if ( itemRect . left < rootRect . left ) {
69+ virtuosoHandle . scrollToIndex ( {
70+ index : targetIndex ,
71+ behavior : 'auto' ,
72+ align : 'start' ,
73+ } ) ;
74+ } else if ( itemRect . right > rootRect . right ) {
75+ virtuosoHandle . scrollToIndex ( {
76+ index : targetIndex ,
77+ behavior : 'auto' ,
78+ align : 'end' ,
79+ } ) ;
80+ } else {
81+ // Image is already in view
82+ }
83+
84+ return ;
85+ } ;
86+
87+ const useScrollableStagingArea = ( rootRef : RefObject < HTMLDivElement > ) => {
88+ const [ scroller , scrollerRef ] = useState < HTMLElement | null > ( null ) ;
89+ const [ initialize , osInstance ] = useOverlayScrollbars ( {
90+ defer : true ,
91+ events : {
92+ initialized ( osInstance ) {
93+ // force overflow styles
94+ const { viewport } = osInstance . elements ( ) ;
95+ viewport . style . overflowX = `var(--os-viewport-overflow-x)` ;
96+ viewport . style . overflowY = `var(--os-viewport-overflow-y)` ;
97+ } ,
98+ } ,
99+ options : {
100+ scrollbars : {
101+ visibility : 'auto' ,
102+ autoHide : 'scroll' ,
103+ autoHideDelay : 1300 ,
104+ theme : 'os-theme-dark' ,
105+ } ,
106+ overflow : {
107+ y : 'hidden' ,
108+ x : 'scroll' ,
109+ } ,
110+ } ,
111+ } ) ;
112+
113+ useEffect ( ( ) => {
114+ const { current : root } = rootRef ;
115+
116+ if ( scroller && root ) {
117+ initialize ( {
118+ target : root ,
119+ elements : {
120+ viewport : scroller ,
121+ } ,
122+ } ) ;
123+ }
124+
125+ return ( ) => {
126+ osInstance ( ) ?. destroy ( ) ;
127+ } ;
128+ } , [ scroller , initialize , osInstance , rootRef ] ) ;
129+
130+ return scrollerRef ;
131+ } ;
8132
9133export const StagingAreaItemsList = memo ( ( ) => {
10134 const canvasManager = useCanvasManagerSafe ( ) ;
11135 const ctx = useCanvasSessionContext ( ) ;
136+ const virtuosoRef = useRef < VirtuosoHandle > ( null ) ;
137+ const rangeRef = useRef < ListRange > ( { startIndex : 0 , endIndex : 0 } ) ;
138+ const rootRef = useRef < HTMLDivElement > ( null ) ;
139+
12140 const items = useStore ( ctx . $items ) ;
13141 const selectedItemId = useStore ( ctx . $selectedItemId ) ;
14142
143+ const context = useMemo ( ( ) => ( { selectedItemId } ) , [ selectedItemId ] ) ;
144+ const scrollerRef = useScrollableStagingArea ( rootRef ) ;
145+
15146 useEffect ( ( ) => {
16147 if ( ! canvasManager ) {
17148 return ;
@@ -20,21 +151,64 @@ export const StagingAreaItemsList = memo(() => {
20151 return canvasManager . stagingArea . connectToSession ( ctx . $selectedItemId , ctx . $progressData , ctx . $isPending ) ;
21152 } , [ canvasManager , ctx . $progressData , ctx . $selectedItemId , ctx . $isPending ] ) ;
22153
154+ useEffect ( ( ) => {
155+ return ctx . $selectedItemIndex . listen ( ( index ) => {
156+ if ( ! virtuosoRef . current ) {
157+ return ;
158+ }
159+
160+ if ( ! rootRef . current ) {
161+ return ;
162+ }
163+
164+ if ( index === null ) {
165+ return ;
166+ }
167+
168+ scrollIntoView ( index , rootRef . current , virtuosoRef . current , rangeRef . current ) ;
169+ } ) ;
170+ } , [ ctx . $selectedItemIndex ] ) ;
171+
172+ const onRangeChanged = useCallback ( ( range : ListRange ) => {
173+ rangeRef . current = range ;
174+ } , [ ] ) ;
175+
23176 return (
24- < Flex position = "relative" maxW = "full " w = "full" h = "72px " >
25- < ScrollableContent overflowX = "scroll" overflowY = "hidden" >
26- < Flex gap = { 2 } w = "full" h = "full" justifyContent = "safe center" >
27- { items . map ( ( item , i ) => (
28- < QueueItemPreviewMini
29- key = { ` ${ item . item_id } -mini` }
30- item = { item }
31- number = { i + 1 }
32- isSelected = { selectedItemId === item . item_id }
33- />
34- ) ) }
35- </ Flex >
36- </ ScrollableContent >
37- </ Flex >
177+ < Box data-overlayscrollbars-initialize = "" ref = { rootRef } position = "relative " w = "full" h = "full " >
178+ < Virtuoso < S [ 'SessionQueueItem' ] , VirtuosoContext >
179+ ref = { virtuosoRef }
180+ context = { context }
181+ data = { items }
182+ horizontalDirection
183+ style = { virtuosoStyles }
184+ itemContent = { itemContent }
185+ components = { components }
186+ rangeChanged = { onRangeChanged }
187+ // Virtuoso expects the ref to be of HTMLElement | null | Window, but overlayscrollbars doesn't allow Window
188+ scrollerRef = { scrollerRef as VirtuosoProps < S [ 'SessionQueueItem' ] , VirtuosoContext > [ 'scrollerRef' ] }
189+ / >
190+ </ Box >
38191 ) ;
39192} ) ;
40193StagingAreaItemsList . displayName = 'StagingAreaItemsList' ;
194+
195+ const itemContent : ItemContent < S [ 'SessionQueueItem' ] , VirtuosoContext > = ( index , item , { selectedItemId } ) => (
196+ < QueueItemPreviewMini
197+ key = { `${ item . item_id } -mini` }
198+ item = { item }
199+ index = { index }
200+ isSelected = { selectedItemId === item . item_id }
201+ />
202+ ) ;
203+
204+ const listSx = {
205+ '& > * + *' : {
206+ pl : 2 ,
207+ } ,
208+ } ;
209+
210+ const components : Components < S [ 'SessionQueueItem' ] , VirtuosoContext > = {
211+ List : forwardRef ( ( { context : _ , ...rest } , ref ) => {
212+ return < Flex ref = { ref } sx = { listSx } { ...rest } /> ;
213+ } ) ,
214+ } ;
0 commit comments