@@ -124,20 +124,22 @@ impl MinimapView {
124124 )
125125 }
126126
127- /// Ensure the viewport rectangle is visible in the minimap
128- /// Adjusts scroll_position so the viewport is always on screen
129- /// Uses interior mutability so it can be called from view()
127+ /// Compute the scroll position (0..1) that would keep the viewport rectangle visible.
130128 ///
131- /// viewport_y and viewport_height are normalized (0-1) relative to full buffer
132- /// content_width and content_height are in pixels
133- fn ensure_viewport_visible ( & self , viewport_y : f32 , viewport_height : f32 , content_width : f32 , content_height : f32 ) {
134- let shared = self . shared_state . lock ( ) ;
135- let avail_width = shared. available_width ;
136- let avail_height = shared. available_height ;
137- drop ( shared) ;
138-
129+ /// This is intentionally **pure** (does not mutate `self.scroll_position`) so it can be
130+ /// called from `view()` without triggering layout invalidation/redraw loops.
131+ fn compute_scroll_to_show_viewport (
132+ & self ,
133+ current_scroll : f32 ,
134+ viewport_y : f32 ,
135+ viewport_height : f32 ,
136+ content_width : f32 ,
137+ content_height : f32 ,
138+ avail_width : f32 ,
139+ avail_height : f32 ,
140+ ) -> f32 {
139141 if avail_width <= 0.0 || avail_height <= 0.0 || content_height <= 0.0 || content_width <= 0.0 {
140- return ;
142+ return current_scroll ;
141143 }
142144
143145 // Scale factor: minimap fills available width
@@ -148,21 +150,20 @@ impl MinimapView {
148150
149151 // If content fits in available space, no scrolling needed
150152 if scaled_content_height <= avail_height {
151- * self . scroll_position . borrow_mut ( ) = 0.0 ;
152- return ;
153+ return 0.0 ;
153154 }
154155
155- // viewport_y and viewport_height are already normalized (0-1)
156+ // viewport_y and viewport_height are normalized (0-1)
156157 // Convert to scaled pixels
157158 let viewport_top_scaled = viewport_y * content_height * scale;
158159 let viewport_height_scaled = viewport_height * content_height * scale;
159160 let viewport_bottom_scaled = viewport_top_scaled + viewport_height_scaled;
160161
161162 // Maximum scroll offset in pixels
162- let max_scroll_px = scaled_content_height - avail_height;
163+ let max_scroll_px = ( scaled_content_height - avail_height) . max ( 0.0 ) ;
163164
164165 // Current scroll offset in pixels
165- let current_scroll = * self . scroll_position . borrow ( ) ;
166+ let current_scroll = current_scroll . clamp ( 0.0 , 1.0 ) ;
166167 let current_scroll_px = current_scroll * max_scroll_px;
167168
168169 // Visible range in scaled pixels
@@ -172,24 +173,20 @@ impl MinimapView {
172173 // If viewport is larger than visible area, align to viewport top to avoid oscillation
173174 if viewport_height_scaled >= avail_height {
174175 let new_scroll_px = viewport_top_scaled;
175- let new_scroll = ( new_scroll_px / max_scroll_px) . clamp ( 0.0 , 1.0 ) ;
176- * self . scroll_position . borrow_mut ( ) = new_scroll;
177- return ;
176+ return ( new_scroll_px / max_scroll_px) . clamp ( 0.0 , 1.0 ) ;
178177 }
179178
180179 // Check if viewport is above visible area
181180 if viewport_top_scaled < visible_top {
182- // Scroll up to show viewport at top
183181 let new_scroll_px = viewport_top_scaled;
184- let new_scroll = ( new_scroll_px / max_scroll_px) . clamp ( 0.0 , 1.0 ) ;
185- * self . scroll_position . borrow_mut ( ) = new_scroll;
182+ ( new_scroll_px / max_scroll_px) . clamp ( 0.0 , 1.0 )
186183 } else if viewport_bottom_scaled > visible_bottom {
187- // Scroll down to show viewport at bottom
188184 let new_scroll_px = viewport_bottom_scaled - avail_height;
189- let new_scroll = ( new_scroll_px / max_scroll_px) . clamp ( 0.0 , 1.0 ) ;
190- * self . scroll_position . borrow_mut ( ) = new_scroll;
185+ ( new_scroll_px / max_scroll_px) . clamp ( 0.0 , 1.0 )
186+ } else {
187+ // If viewport is within visible area, don't change scroll
188+ current_scroll
191189 }
192- // If viewport is within visible area, don't change scroll
193190 }
194191
195192 /// Update the minimap view state
@@ -255,12 +252,24 @@ impl MinimapView {
255252 let content_width = content_width_u32 as f32 ;
256253 let content_height = content_height_f32;
257254
258- // Auto-scroll to keep viewport visible
259- self . ensure_viewport_visible ( viewport_info. y , viewport_info. height , content_width, content_height) ;
255+ // Compute a scroll position that keeps the viewport visible.
256+ // Do NOT mutate widget state from view() (can trigger layout invalidation loops).
257+ let scroll_normalized = {
258+ let current = * self . scroll_position . borrow ( ) ;
259+ self . compute_scroll_to_show_viewport (
260+ current,
261+ viewport_info. y ,
262+ viewport_info. height ,
263+ content_width,
264+ content_height,
265+ avail_width,
266+ avail_height,
267+ )
268+ } ;
260269
261270 // Calculate which tiles to select based on visible area
262271 let tile_height = TILE_HEIGHT as f32 ;
263- let scroll_normalized = * self . scroll_position . borrow ( ) ;
272+ let scroll_normalized = scroll_normalized ;
264273
265274 let max_tile_idx = ( ( content_height / tile_height) . ceil ( ) . max ( 1.0 ) as i32 ) - 1 ;
266275
0 commit comments