use galaxy_core::features::FeatureFlag; use galaxyui::{elements::ScrollOffset, units::Pixels, ViewContext, ViewHandle}; #[cfg(not(target_family = "wasm"))] use galaxyui::{AppContext, WeakViewHandle}; #[cfg(not(target_family = "wasm"))] use super::FILE_HEADER_HEIGHT; use super::{CodeReviewView, CodeReviewViewState}; use crate::code::editor::model::StableEditorLine; use crate::code::local_code_editor::LocalCodeEditorView; /// Context for preserving scroll position across file diff content changes. /// The scroll position can be in different regions of the file diff item. #[derive(Clone, Debug)] #[cfg_attr(target_family = "wasm", allow(dead_code))] pub(super) enum RelocatableScrollContext { /// Scroll position is in the file header region. /// Stores the pixel offset from the top of the header. Header { offset: Pixels }, /// Scroll is on a line in the editor content (current or removed). /// The [`StableEditorLine`] contains an internal anchor that /// automatically tracks through buffer edits. EditorLine { stable_line: StableEditorLine, intra_line_offset: Pixels, }, /// Scroll position is in the footer region. /// Stores the pixel offset from the top of the footer. Footer { offset: Pixels }, } impl CodeReviewView { /// Computes the adjusted item-relative scroll offset for a file diff item /// based on the captured scroll context. Called by the `ListState` /// adjustment closure when an item's height changes during layout. #[cfg(not(target_family = "wasm"))] pub(super) fn adjust_scroll_offset( view_handle: &WeakViewHandle, index: usize, captured_context: &RelocatableScrollContext, app: &AppContext, ) -> Option { if !FeatureFlag::CodeReviewScrollPreservation.is_enabled() { return None; } // The adjustment function returns item-relative offsets (offset_from_start), // NOT absolute positions. The ListState stores the result directly in // scroll_top.offset_from_start, which is relative to the current scroll item. match captured_context { RelocatableScrollContext::Header { offset } => Some(*offset), RelocatableScrollContext::EditorLine { stable_line, intra_line_offset, } => { let view_handle = view_handle.upgrade(app)?; let view = view_handle.as_ref(app); let CodeReviewViewState::Loaded(state) = view.state() else { return None; }; let editor_state = state .file_states .get_index(index)? .1 .editor_state .as_ref()?; let editor_view = editor_state.editor.as_ref(app).editor().as_ref(app); let line_offset = editor_view.line_top(stable_line, app)?; Some(Pixels::new(FILE_HEADER_HEIGHT) + line_offset + *intra_line_offset) } RelocatableScrollContext::Footer { offset } => { let view_handle = view_handle.upgrade(app)?; let view = view_handle.as_ref(app); let CodeReviewViewState::Loaded(state) = view.state() else { return None; }; let editor_state = state .file_states .get_index(index)? .1 .editor_state .as_ref()?; let content_height = editor_state .editor .as_ref(app) .editor() .as_ref(app) .content_height(app); Some(Pixels::new(FILE_HEADER_HEIGHT) + content_height + *offset) } } } /// Computes the scroll preservation context for the given index and editor. /// Returns `Some(context)` only if the index is the currently scrolled item. /// Detects whether scroll is in header, editor content, or footer region. #[cfg(not(target_family = "wasm"))] pub(super) fn compute_scroll_context_for_index( &self, index: usize, editor: &ViewHandle, ctx: &mut ViewContext, ) -> Option { // Only compute context if this is the currently scrolled item let current_scroll_index = self.viewported_list_state.get_scroll_index(); if current_scroll_index != index { return None; } // Get the scroll offset within this item let scroll_offset_in_item = self.viewported_list_state.get_scroll_offset(); let file_header_height = Pixels::new(FILE_HEADER_HEIGHT); // Check if scroll is in the header region if scroll_offset_in_item < file_header_height { return Some(RelocatableScrollContext::Header { offset: scroll_offset_in_item, }); } // Compute offset relative to editor content let scroll_offset_in_editor = scroll_offset_in_item - file_header_height; // Check footer region using the view-level accessor. let editor_view = editor.as_ref(ctx).editor(); let content_height = editor_view.as_ref(ctx).content_height(ctx); if scroll_offset_in_editor >= content_height { return Some(RelocatableScrollContext::Footer { offset: scroll_offset_in_editor - content_height, }); } // Clone the model handle so immutable borrows on ctx can be released // before the anchor creation which requires a mutable borrow. let editor_model_handle = editor_view.as_ref(ctx).model.clone(); // Identify the line and create anchors (requires mutable context). let (stable_line, intra_line_offset) = editor_model_handle.update(ctx, |model, ctx| { model.line_at_vertical_offset(scroll_offset_in_editor, ctx) })?; Some(RelocatableScrollContext::EditorLine { stable_line, intra_line_offset, }) } /// Wasm stub - scroll preservation not supported #[cfg(target_family = "wasm")] pub(super) fn compute_scroll_context_for_index( &self, _index: usize, _editor: &ViewHandle, _ctx: &mut ViewContext, ) -> Option { None } /// Called when scrolling settles (via debounced scroll events). /// Computes and stores the current scroll context on the ListState /// so the explicit invalidation path can adjust scroll position /// when a file diff item's height changes. fn on_scroll_settled(&mut self, _scroll_offset: ScrollOffset, ctx: &mut ViewContext) { let scroll_index = self.viewported_list_state.get_scroll_index(); let Some(repo) = self.active_repo.as_ref() else { return; }; let CodeReviewViewState::Loaded(state) = &repo.state else { return; }; let Some((_, file_state)) = state.file_states.get_index(scroll_index) else { return; }; let Some(editor_state) = &file_state.editor_state else { return; }; let editor = editor_state.editor(); if let Some(context) = self.compute_scroll_context_for_index(scroll_index, editor, ctx) { self.viewported_list_state.set_scroll_context(Some(context)); } } /// Sets up scroll tracking from a scroll event receiver. /// Calls [`Self::on_scroll_settled`] on every scroll event so the /// scroll context is always up-to-date. /// No-op when [`FeatureFlag::CodeReviewScrollPreservation`] is disabled. pub(super) fn setup_scroll_tracking( scroll_rx: async_channel::Receiver, ctx: &mut ViewContext, ) { if FeatureFlag::CodeReviewScrollPreservation.is_enabled() { ctx.spawn_stream_local(scroll_rx, Self::on_scroll_settled, |_, _| {}); } } }